Compare commits

...
Author SHA1 Message Date
Jonathan Lipps ea00e3aa18 chore: publish
- appium@2.16.0
 - @appium/base-driver@9.16.0
 - @appium/base-plugin@2.3.1
 - @appium/docutils@1.0.30
 - @appium/driver-test-support@0.7.5
 - @appium/eslint-config-appium-ts@1.0.3
 - @appium/execute-driver-plugin@4.0.2
 - @appium/fake-driver@5.7.1
 - @appium/fake-plugin@3.2.1
 - @appium/images-plugin@3.0.28
 - @appium/opencv@3.0.8
 - @appium/plugin-test-support@0.3.50
 - @appium/schema@0.8.0
 - @appium/support@6.0.4
 - @appium/test-support@3.1.4
 - @appium/types@0.25.0
 - @appium/universal-xml-plugin@1.0.28
2025-02-19 12:15:46 -08:00
Jonathan Lipps f8ddac629d chore: remove sauce labs as project sponsor (#21008) 2025-02-19 11:58:09 -08:00
Mykola Mokhnach eb1b156146 feat(appium): Add a command line parameter to configure HTTP server request timeout (#21003) 2025-02-19 20:45:20 +01:00
renovate[bot] c8ac9c40bb fix(universal-xml-plugin): update dependency fast-xml-parser to v5 (#21006) 2025-02-19 20:44:18 +01:00
Kim Arnett c4337fc3e7 docs(appium): remove axe DevTools for Mobile drivers from Appium other drivers list (#21007) 2025-02-19 17:59:32 +01:00
renovate[bot] 686a630fbd fix(execute-driver-plugin): update dependency webdriverio to v9.9.3 (#21005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 16:09:58 +00:00
renovate[bot] 5a227d23d2 chore(deps): update dependency eslint-import-resolver-typescript to v3.8.2 (#21004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 12:15:51 +00:00
renovate[bot] c7900f4a16 fix(universal-xml-plugin): update dependency fast-xml-parser to v4.5.2 (#21002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:41:40 +00:00
renovate[bot] 312a1c53f3 chore(deps): update eslint-related packages (#21001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 10:27:11 +00:00
renovate[bot] ca456e5236 fix(driver-test-support): update dependency chai to v5.2.0 (#20998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 09:50:41 +01:00
renovate[bot] 3dc7336b5f fix(types): update dependency type-fest to v4.35.0 (#20999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:21:26 +01:00
renovate[bot] 224f5247b0 fix(support): update dependency form-data to v4.0.2 (#20997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 13:55:17 +00:00
renovate[bot] c6a080f7ad fix(execute-driver-plugin): update dependency webdriverio to v9.9.1 (#20996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 10:11:14 +00:00
renovate[bot] b3c877f66d fix(execute-driver-plugin): update dependency webdriverio to v9.9.0 (#20994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 19:22:49 +00:00
Kazuaki Matsuo 419661013b chore(appium): returns this.caps for /session/:sessionId/appium/capabilities (#20979)
* return the same value of getSession

* chore: update docs and implementation

* remove event stuff

* add type in return

* add AppiumSessionCapabilities type

* update the syntax

* rename

* add test in e2e

* remove unnecessary type

* use fake driver name
2025-02-14 08:08:42 -08:00
renovate[bot] 9b08c209f9 chore(deps): update dependency eslint-import-resolver-typescript to v3.8.0 (#20995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 15:27:43 +00:00
renovate[bot] ab07a0bc90 chore(deps): update dependency prettier to v3.5.1 (#20993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 10:38:46 +00:00
renovate[bot] 91d9a3ef5d chore(deps): update dependency @types/node to v22.13.4 (#20992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 07:15:48 +00:00
renovate[bot] 4c9a42062c chore(docutils): update dependency mkdocs-material to v9.6.4 (#20990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 20:08:50 +00:00
renovate[bot] 30be097e15 chore(deps): update dependency @types/node to v22.13.2 (#20989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 14:46:02 +00:00
Kazuaki Matsuo 97ae82b2a2 docs(appium): use event endpoint to get event timing in zh resource (#20985) 2025-02-12 01:33:45 -08:00
Kazuaki Matsuo aea0ccfa00 docs(appium): use event endpoint to get event timing (#20984)
* docs(appium): use event endpoint to get event timing
2025-02-12 01:32:12 -08:00
Edgars Eglītis e934b61856 chore(renovate): add @types/supports-color to blocklist (#20983) 2025-02-12 10:36:17 +02:00
renovate[bot] 99e96502b9 chore(deps): update dependency eslint to v9.20.1 (#20982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 07:32:54 +00:00
Kazuaki Matsuo 848f2b2396 chore(appium): handle getAppiumSessions as non session id commands (#20981) 2025-02-11 23:16:56 -08:00
renovate[bot] a1e3890681 chore(deps): update dependency typescript-eslint to v8.24.0 (#20978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 09:36:32 +00:00
Kazuaki Matsuo 21e2aabc39 fix(appium): add await for async function (#20974) 2025-02-10 10:01:24 -08:00
renovate[bot] 0a7490ed53 fix(types): update dependency type-fest to v4.34.1 (#20971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 05:19:14 +00:00
renovate[bot] 1c2c119461 chore(deps): update eslint-related packages to v9.20.0 (#20968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 00:57:15 +00:00
renovate[bot] 49da3c6c35 chore(deps): update dependency prettier to v3.5.0 (#20970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-09 22:22:04 +00:00
renovate[bot] 0b27739768 chore(docutils): update dependency mkdocs-material to v9.6.3 (#20967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-08 13:59:58 +00:00
renovate[bot] ce32226cb5 fix(execute-driver-plugin): update dependency webdriverio to v9.8.0 (#20964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 00:26:28 +00:00
Mykola Mokhnach f8b57999e5 feat(base-driver): Print the closest match if the given script did not match (#20956) 2025-02-06 08:56:13 +01:00
renovate[bot] 52c6c6933b chore(deps): update dependency typescript-eslint to v8.23.0 (#20962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 17:52:25 +00:00
renovate[bot] 4c089440e6 fix(support): update dependency semver to v7.7.1 (#20961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 15:05:21 +00:00
renovate[bot] 94f93a1421 chore(docutils): update dependency mkdocs-material to v9.6.2 (#20958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 12:25:37 +00:00
renovate[bot] b3018fcb76 chore(deps): update dependency @types/node to v22.13.1 (#20957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 08:58:19 +00:00
Mykola Mokhnach 3cfe3af1c8 fix(appium): Wait for upstream web socket to open before sending data to it (#20953) 2025-02-02 22:50:08 +01:00
Edgars Eglītis 70f154da52 chore: move or remove some main dependencies (#20952) 2025-02-02 11:57:13 +02:00
renovate[bot] 8a87698cc2 chore(deps): update definitelytyped (#20950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-01 21:28:27 +00:00
Kazuaki Matsuo 7edd3bdc11 chore: use RegExp to check session id requirement (#20947)
* chore: use regext to check session id requirement

* update docs

* add test case

* use match of path-to-regexp

* use another name to avoid conflict

* accept appium prefix for non-session id
2025-02-01 01:03:15 -08:00
renovate[bot] 98da49ff58 chore(docutils): update dependency mkdocs-material to v9.6.1 (#20951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-01 08:47:41 +00:00
renovate[bot] 81ebd758cb fix(support): update dependency semver to v7.7.0 (#20948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 19:14:31 +00:00
renovate[bot] f5e0819f50 fix(execute-driver-plugin): update dependency webdriverio to v9.7.2 (#20944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-29 13:59:11 +00:00
Kazuaki Matsuo eeb59cab07 feat: add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future (#20936)
* feat: add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future

* add a dymmy implementation

* move entire deprecated endpoints in bottom of each section

* define GET_APPIUM_SESSION_CAPABILITIES and add it to isAppiumDriverCommand

* adds _COMMAND

* remove unnecessary method definition

* move deprecated endpoints

* add temporary implementation
2025-01-28 10:14:51 -08:00
renovate[bot] 75a0df2975 chore(deps): update dependency typescript-eslint to v8.22.0 (#20943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 17:11:41 +00:00
renovate[bot] f7c63ee5d8 fix(driver-test-support): update definitelytyped (#20942)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 14:43:46 +00:00
Mykola Mokhnach 841a835153 chore(base-driver): Cleanup unnecessary command definitions (#20939) 2025-01-27 11:05:07 +01:00
Mykola Mokhnach a6b6077ecd feat: Add /appium/extensions API to list available extensions (#20931) 2025-01-25 17:02:41 +01:00
renovate[bot] caff2c98a9 chore(deps): update eslint-related packages to v9.19.0 (#20935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 11:13:42 +01:00
renovate[bot] 50fa5d670b fix(execute-driver-plugin): update dependency webdriverio to v9.7.1 (#20934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-25 11:13:29 +01:00
renovate[bot] 57809826a4 fix(execute-driver-plugin): update dependency webdriverio to v9.7.0 (#20930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 20:04:53 +01:00
Dalton Alexandre d5ab2c1f31 docs(appium): Add Unofficial SwiftAppium Client 2025-01-24 19:51:47 +01:00
Mykola Mokhnach 2635dcb457 feat: Add BiDi commands to the listCommands API output (#20925) 2025-01-24 19:48:54 +01:00
renovate[bot] e7541f88fb chore(deps): update dependency mocha to v11.1.0 (#20929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 18:17:12 +00:00
renovate[bot] 1f65cf6555 fix(deps): update definitelytyped (#20928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 12:33:21 +00:00
Mykola Mokhnach 059f1cb698 feat(base-driver): Add an API to list commands (#20914) 2025-01-23 19:07:16 +01:00
renovate[bot] 2409d3223a fix(types): update dependency type-fest to v4.33.0 (#20923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:25:44 +00:00
renovate[bot] 7e1f1e5aad fix(universal-xml-plugin): update dependency @xmldom/xmldom to v0.9.7 (#20918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 11:24:37 +02:00
renovate[bot] 4180270f0b chore(deps): update dependency typescript-eslint to v8.21.0 (#20919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 13:50:10 +00:00
Mykola Mokhnach fe4ffc159a feat(types): Add an optional info field to base method type definitions (#20917) 2025-01-20 14:14:07 +01:00
renovate[bot] 4cda1e48ef chore(docutils): update dependency mkdocs-material to v9.5.50 (#20915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 13:06:06 +00:00
renovate[bot] c5da98a5d9 chore(eslint-config-appium-ts): update dependency eslint-config-prettier to v10 (#20911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 23:06:23 +01:00
Mykola Mokhnach 3c58054a47 fix: Avoid "Unhandled rejection" error on mjpeg connection failure (#20909) 2025-01-16 17:41:29 +01:00
renovate[bot] 2d5d4bec02 chore(deps): update dependency @types/node to v22.10.7 (#20910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 12:42:33 +00:00
renovate[bot] 8fb0da34bb fix(docutils): update dependency consola to v3.4.0 (#20908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 17:55:57 +00:00
renovate[bot] b686178f93 chore(deps): update dependency typescript-eslint to v8.20.0 (#20907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 13:26:38 +00:00
renovate[bot] 803f69726b chore(deps): update dependency json-schema-to-typescript to v15.0.4 (#20905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 20:56:49 +00:00
renovate[bot] 7764e71257 chore(deps): update dependency @types/node to v22.10.6 (#20904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 14:24:42 +00:00
renovate[bot] c60fdc8d84 fix(execute-driver-plugin): update dependency webdriverio to v9.5.7 (#20903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 14:10:37 +00:00
renovate[bot] fbc85308a5 fix(types): update dependency type-fest to v4.32.0 (#20900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:43:42 +00:00
renovate[bot] 26eda4775e chore(deps): update dependency @types/diff to v7 (#20901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 18:44:54 +02:00
renovate[bot] e96908fcbb chore(deps): update eslint-related packages to v9.18.0 (#20899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 14:01:26 +00:00
renovate[bot] 7f91d5cba8 fix(execute-driver-plugin): update dependency webdriverio to v9.5.4 (#20898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 09:55:34 +00:00
renovate[bot] 561aff641a fix(support): update dependency uuid to v11.0.5 (#20896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 19:13:11 +00:00
renovate[bot] 874147449a fix(execute-driver-plugin): update dependency webdriverio to v9.5.3 (#20895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 13:37:25 +00:00
renovate[bot] 6706a0037e fix(docutils): update dependency typescript to v5.7.3 (#20893)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 18:25:56 +00:00
renovate[bot] 6d62389d26 chore(deps): update dependency typescript-eslint to v8.19.1 (#20892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 14:03:13 +00:00
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
207 changed files with 12524 additions and 6704 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))
-5
View File
@@ -204,11 +204,6 @@ contribution of project development and leadership!
</a>
</p>
<p align="center">
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner"><img src="packages/appium/docs/overrides/assets/images/sponsor-logo-sauce.png"
width="300" alt="Sauce Labs" /></a>
</p>
### Other Sponsors
A full list of sponsors is available at our [Sponsors page](https://appium.io/docs/en/latest/sponsors/).
+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/**',
],
},
];
+8603 -4589
View File
File diff suppressed because it is too large Load Diff
+39 -17
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,44 +85,65 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/js": "9.20.0",
"@tsconfig/node14": "14.1.2",
"@types/argparse": "2.0.17",
"@types/archiver": "6.0.3",
"@types/async-lock": "1.4.2",
"@types/base64-stream": "1.0.5",
"@types/bluebird": "3.5.42",
"@types/chai": "5.0.1",
"@types/chai-as-promised": "8.0.1",
"@types/diff": "6.0.0",
"@types/diff": "7.0.1",
"@types/express": "5.0.0",
"@types/jsftp": "2.1.5",
"@types/json-schema": "7.0.15",
"@types/klaw": "3.0.6",
"@types/lockfile": "1.0.4",
"@types/lodash": "4.17.15",
"@types/method-override": "3.0.0",
"@types/mocha": "10.0.10",
"@types/node": "22.10.1",
"@types/mv": "2.1.4",
"@types/ncp": "2.0.8",
"@types/node": "22.13.4",
"@types/pluralize": "0.0.33",
"@types/semver": "7.5.8",
"@types/serve-favicon": "2.5.7",
"@types/shell-quote": "1.7.5",
"@types/sinon": "17.0.3",
"@types/sinon-chai": "4.0.0",
"@types/source-map-support": "0.5.10",
"@types/supports-color": "8.1.3",
"@types/teen_process": "2.0.4",
"@types/ws": "8.5.13",
"@types/uuid": "10.0.0",
"@types/which": "3.0.4",
"@types/wrap-ansi": "3.0.0",
"@types/ws": "8.5.14",
"@types/xmldom": "0.1.34",
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"asyncbox": "3.0.0",
"chai": "5.1.2",
"chai": "5.2.0",
"chai-as-promised": "8.0.1",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "5.0.0",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
"eslint-find-rules": "4.2.0",
"eslint-import-resolver-typescript": "3.7.0",
"cross-env": "7.0.3",
"eslint": "9.20.1",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-typescript": "3.8.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-mocha": "10.5.0",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-promise": "7.2.1",
"finalhandler": "1.3.1",
"get-port": "5.1.1",
"json-schema-to-typescript": "15.0.3",
"json-schema-to-typescript": "15.0.4",
"lerna": "8.1.9",
"log-symbols": "4.1.0",
"midnight-smoker": "8.0.0",
"mjpeg-consumer": "2.0.0",
"mjpeg-server": "0.3.1",
"mocha": "11.0.1",
"mocha": "11.1.0",
"npm-run-all2": "6.2.6",
"prettier": "3.4.2",
"prettier": "3.5.1",
"rewiremock": "3.14.5",
"rimraf": "5.0.10",
"serve-static": "1.16.2",
@@ -130,9 +151,10 @@
"sync-monorepo-packages": "1.0.2",
"ts-node": "10.9.2",
"tsd": "0.31.2",
"typescript": "5.7.2",
"typescript": "5.7.3",
"typescript-eslint": "8.24.1",
"validate.js": "0.13.1",
"webdriverio": "8.40.6",
"webdriverio": "9.9.3",
"ws": "8.18.0",
"yaml-js": "0.3.1"
},
+78
View File
@@ -3,6 +3,84 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.16.0](https://github.com/appium/appium/compare/appium@2.15.0...appium@2.16.0) (2025-02-19)
### Features
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
### Bug Fixes
* **appium:** add await for async function ([#20974](https://github.com/appium/appium/issues/20974)) ([21e2aab](https://github.com/appium/appium/commit/21e2aabc391fefe0dff8e29fe1cd6be908d5b3d0))
* **appium:** Wait for upstream web socket to open before sending data to it ([#20953](https://github.com/appium/appium/issues/20953)) ([3cfe3af](https://github.com/appium/appium/commit/3cfe3af1c8d88c8ec79d2d89c7e76d697205c86b))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.3 ([#20895](https://github.com/appium/appium/issues/20895)) ([8741474](https://github.com/appium/appium/commit/874147449a26bbec03b08e7e95fb9e4c6b5881af))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.4 ([#20898](https://github.com/appium/appium/issues/20898)) ([7f91d5c](https://github.com/appium/appium/commit/7f91d5cba850100124088d774ba8704e3765863f))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.7 ([#20903](https://github.com/appium/appium/issues/20903)) ([c60fdc8](https://github.com/appium/appium/commit/c60fdc8d847b4aeed2d8b8307180194ee32e5ca9))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.0 ([#20930](https://github.com/appium/appium/issues/20930)) ([5780982](https://github.com/appium/appium/commit/57809826a410b71dca155f59e081f77dcaf671ed))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.1 ([#20934](https://github.com/appium/appium/issues/20934)) ([50fa5d6](https://github.com/appium/appium/commit/50fa5d670bd967200a78e5b4b8a54d5c9b85901b))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.2 ([#20944](https://github.com/appium/appium/issues/20944)) ([f5e0819](https://github.com/appium/appium/commit/f5e0819f505b55421b7d260f69a41ae980390a91))
* **execute-driver-plugin:** update dependency webdriverio to v9.8.0 ([#20964](https://github.com/appium/appium/issues/20964)) ([ce32226](https://github.com/appium/appium/commit/ce32226cb57fb3f07bf4862dbbfcba227ff1230c))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.0 ([#20994](https://github.com/appium/appium/issues/20994)) ([b3c877f](https://github.com/appium/appium/commit/b3c877f66ddf3ccf7a25ca1f7a3952c660bd7dda))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.1 ([#20996](https://github.com/appium/appium/issues/20996)) ([c6a080f](https://github.com/appium/appium/commit/c6a080f7ade03a49a274ce6fbc84629406fdc2a7))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.3 ([#21005](https://github.com/appium/appium/issues/21005)) ([686a630](https://github.com/appium/appium/commit/686a630fbdf525db16b42fdfc1bbdaf6426b41d0))
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [2.15.0](https://github.com/appium/appium/compare/appium@2.14.1...appium@2.15.0) (2025-01-08)
### Features
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
## [2.14.1](https://github.com/appium/appium/compare/appium@2.14.0...appium@2.14.1) (2025-01-06)
### Bug Fixes
* **appium:** Do not update BiDi commands for drivers which don't support it ([#20879](https://github.com/appium/appium/issues/20879)) ([13d8b03](https://github.com/appium/appium/commit/13d8b03cc185c5f4e0631002614981ee6f86fab4))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.1 ([#20875](https://github.com/appium/appium/issues/20875)) ([93e4288](https://github.com/appium/appium/commit/93e4288253c07a844e6e10f0dd040b1a40eb92ff))
* **support:** update dependency teen_process to v2.2.3 ([#20882](https://github.com/appium/appium/issues/20882)) ([488e5de](https://github.com/appium/appium/commit/488e5dea82d3c525acf483952570e32f319f3722))
## [2.14.0](https://github.com/appium/appium/compare/appium@2.13.1...appium@2.14.0) (2025-01-02)
### Features
* **appium:** Add session.status BiDi command ([#20839](https://github.com/appium/appium/issues/20839)) ([64e768e](https://github.com/appium/appium/commit/64e768efb7bebd6b5a24d55206d1cad00812777c))
* **appium:** allow drivers to define their own bidi commands ([#20828](https://github.com/appium/appium/issues/20828)) ([a917ec6](https://github.com/appium/appium/commit/a917ec6ceda2166fb3dcbff6b2768f700db9e103))
* **appium:** Update entry point resolution logic for ESM extensions ([#20866](https://github.com/appium/appium/issues/20866)) ([bbfc040](https://github.com/appium/appium/commit/bbfc04026f0629d89d8b313616b852896481d08d))
### Bug Fixes
* **appium:** update CLI help messages ([#20825](https://github.com/appium/appium/issues/20825)) ([04e2d11](https://github.com/appium/appium/commit/04e2d1154b67c34607ab7a1e2a44b9b4bcfb998f))
* **docutils:** update dependency yaml to v2.7.0 ([#20870](https://github.com/appium/appium/issues/20870)) ([389a3e8](https://github.com/appium/appium/commit/389a3e83ac0069504338f7182a4e938cd391de7e))
* **execute-driver-plugin:** update dependency webdriverio to v9.4.5 ([#20847](https://github.com/appium/appium/issues/20847)) ([4474412](https://github.com/appium/appium/commit/4474412a60a82556f43fd3f3fb4f4ea2e4bc11ee))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.0 ([#20871](https://github.com/appium/appium/issues/20871)) ([990a066](https://github.com/appium/appium/commit/990a0667e3442bf01aaf67327066517d6310c371))
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
## [2.13.1](https://github.com/appium/appium/compare/appium@2.13.0...appium@2.13.1) (2024-12-05)
-5
View File
@@ -204,11 +204,6 @@ contribution of project development and leadership!
</a>
</p>
<p align="center">
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner"><img src="packages/appium/docs/overrides/assets/images/sponsor-logo-sauce.png"
width="300" alt="Sauce Labs" /></a>
</p>
### Other Sponsors
A full list of sponsors is available at our [Sponsors page](https://appium.io/docs/en/latest/sponsors/).
+1
View File
@@ -34,6 +34,7 @@ below.
|`--driver`|Driver-specific configuration. Keys should correspond to driver package names|object|||
|`--drivers-import-chunk-size`|The maximum amount of drivers that could be imported in parallel on server startup|number|`3`||
|`--keep-alive-timeout`|Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. Setting this to `0` disables the timeout.|integer|`600`|`-ka`|
|`--request-timeout`|Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. A value of 0 disables the timeout. Set it to a non-zero value to protect against potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.|integer|`3600`||
|`--local-timezone`|Use local timezone for timestamps|boolean|`false`||
|`--log`|Also send log output to this file|string||`-g`|
|`--log-filters`|One or more log filtering rules|array|||
@@ -47,7 +47,13 @@ A session data object
`GET` **`/session/:sessionId`**
Returns capabilities for the session and event history (if applicable)
Returns capabilities for the session and event history (if applicable).
!!! warning "Deprecated"
Please use `getAppiumSessionCapabilities` to get the session capabilities.
Please use `getLogEvents` to get event history.
<!-- comment source: multiple -->
@@ -57,6 +63,20 @@ Returns capabilities for the session and event history (if applicable)
A session data object
### `getAppiumSessionCapabilities`
`GET` **`/session/:sessionId/appium/capabilities`**
Returns capabilities for the session.
<!-- comment source: multiple -->
#### Response
`AppiumSessionCapabilities`
A session data object
### `findElement`
`POST` **`/session/:sessionId/element`**
@@ -320,9 +320,9 @@ your driver of the appropriate name, and it will be called when the BiDi command
the client. To see which specific names you should use for BiDi commands, have a look at
[bidi-commands.js](https://github.com/appium/appium/blob/master/packages/base-driver/lib/protocol/bidi-commands.js)
Currently, you also need to define a `doesSupportBidi` field on your driver instances, and ensure
it is set to `true`. Appium will not turn on its Websocket servers for your driver and set up any
handlers unless your driver says that it supports BiDi in this way.
You are not limited to BiDi commands that are defined in the official BiDi specification. If you
wish to define new commands, you may do so; you just need to tell Appium about them! See
[below](#extend-the-existing-protocol-with-new-commands) for more information.
### Implement element finding
@@ -558,11 +558,9 @@ remote endpoint you're proxying to. Appium will take care of all the proxying fo
### Proxy BiDi commands to another BiDi implementation
All of the above about proxying WebDriver commands is conceptually also valid for proxying BiDi
commands specifically. In order to enable BiDi proxying, you need to:
1. Set the `doesSupportBidi` field on your driver instances to `true`.
1. Implement `get bidiProxyUrl` on your driver. This should return a Websocket URL which is the
address of the upstream socket you want BiDi commands to be proxied to.
commands specifically. In order to enable BiDi proxying, you need to implement `get bidiProxyUrl`
on your driver. This should return a Websocket URL which is the address of the upstream socket you
want BiDi commands to be proxied to.
The intended pattern here is for you to start a session on the upstream implementation, check
whether it has an active BiDi socket in the returned capabilities (e.g., the `webSocketUrl`
@@ -574,9 +572,10 @@ to the upstream connection.
You may find that the existing commands don't cut it for your driver. If you want to expose
behaviours that don't map to any of the existing commands, you can create new commands in one of
two ways:
three ways:
1. Extending the WebDriver protocol and creating client-side plugins to access the extensions
1. Extending the classic WebDriver protocol and creating client-side plugins to access the extensions via the classic HTTP interface
1. Extending the WebDriver BiDi protocol with new modules and methods, accessed from a client via the BiDi interface
1. Overloading the Execute Script command by defining [Execute
Methods](../guides/execute-methods.md)
@@ -606,7 +605,45 @@ won't have nice client-side functions designed to target these endpoints. So you
create and release client-side plugins for each language you want to support (directions or
examples can be found at the relevant client docs).
An alternative to this way of doing things is to overload a command which all WebDriver clients
The second way of adding new commands is adding them as BiDi commands (accessed via the BiDi
websocket interface, rather than the classic HTTP interface). BiDi commands come in two parts:
a "module", which is basically a container or namespace, and a "command", which is the name of your
new command.
As with the first method, you teach Appium to recognize your new BiDi commands by adding a static
field to your driver class, called `newBidiCommands`. It has a format similar to `newMethodMap`.
Basically it encapsulates information about the BiDi module, BiDi command name, reference to your
driver instance method that will handle the command, and required and optional parameters. Here's
an example of a `newBidiCommands` as implemented on an imaginary driver:
```js
static newBidiCommands = {
'appium:video': {
startFramerateCapture: {
command: 'startFrameCap',
params: {
required: ['videoSource'],
optional: ['showOnScreen'],
}
},
stopFramerateCapture: {
command: 'stopFrameCap',
},
}
};
```
In this imaginary example, we have defined two new BiDi commands:
`appium:video.startFramerateCapture` and `appium:video.stopFramerateCapture`. Note first of all
that, because we are defining a custom BiDi command, we should include a 'vendor prefix' (in this
case, `appium:`, though you should pick something that represents your project). The first command
takes a required and an optional parameter, and the second does not. When combined with generic
BiDi support in your driver (see [the section on BiDi](#implement-webdriver-bidi-commands) above),
and given an implementation of the appropriate methods on your driver (e.g. `startFrameCap` and
`stopFrameCap` in this example), clients would be able to send these BiDi commands using whatever
mechanism normally exists for doing so in the client library.
An alternative to these other ways of doing things is to overload a command which all WebDriver clients
have access to already: Execute Script. Appium provides some a convenient tool for making this
easy. Let's say you are building a driver for stereo system called `soundz`, and you wanted to
create a command for playing a song by name. You could expose this to your users in such a way that
@@ -657,6 +694,10 @@ A couple notes about this system:
1. The `executeMethod` helper will reject with an error if a script name doesn't match one of the
script names defined as a command in `executeMethodMap`, or if there are missing parameters.
One of the nice things about the Execute Method strategy is that methods implemented in this way
will be available via the classic or BiDi interfaces (since they will result in the same Appium
handlers being called).
### Build Appium Doctor checks
@@ -711,7 +752,7 @@ in `this.currentCpuLoad`):
```js
this.eventEmitter.emit('bidiEvent', {
method: 'system.cpu',
method: 'appium:system.cpu',
params: {load: this.currentCpuLoad},
})
```
@@ -325,9 +325,12 @@ If you want to offer functionality that doesn't map to any of the existing comma
drivers, you can create new commands in one of two ways, just as is possible for drivers:
1. Extending the WebDriver protocol and creating client-side plugins to access the extensions
1. Extending the WebDriver BiDi protocol with new modules and methods, accessed from a client via the BiDi interface
1. Overloading the Execute Script command by defining [Execute
Methods](../guides/execute-methods.md)
#### Extending the HTTP Protocol
If you want to follow the first path, you can direct Appium to recognize new methods and add them
to its set of allowed HTTP routes and command names. You do this by assigning the `newMethodMap`
static variable in your driver class to an object of the same form as Appium's `routes.js` object.
@@ -368,7 +371,60 @@ clients won't have nice client-side functions designed to target these endpoints
to create and release client-side plugins for each language you want to support (directions or
examples can be found at the relevant client docs).
An alternative to this way of doing things is to overload a command which all WebDriver clients
#### Extending the BiDi Protocol
You can also make new commands accessible via the WebDriver BiDi (WebSocket-based) protocol. BiDi
commands come in two parts: a "module", which is basically a container or namespace, and
a "command", which is the name of your new command.
As with the first method, you teach Appium to recognize your new BiDi commands by adding a static
field to your driver class, called `newBidiCommands`. It has a format similar to `newMethodMap`.
Basically it encapsulates information about the BiDi module, BiDi command name, reference to your
driver instance method that will handle the command, and required and optional parameters. Here's
an example of a `newBidiCommands` as implemented on an imaginary driver:
```js
static newBidiCommands = {
'appium:video': {
startFramerateCapture: {
command: 'startFrameCap',
params: {
required: ['videoSource'],
optional: ['showOnScreen'],
}
},
stopFramerateCapture: {
command: 'stopFrameCap',
},
}
};
```
In this imaginary example, we have defined two new BiDi commands: `appium:video.startFramerateCapture` and
`appium:video.stopFramerateCapture`. The first command takes a required and an optional parameter, and the
second does not. When you have implemented the `startFrameCap` and `stopFrameCap` methods on your
plugin class, they will be called whenever the BiDi commands are triggered by a client. The
signatures for these methods would look as follows:
```js
async startFrameCap(next: () => Promise<any>, driver: DriverClass, videoSource: string, showOnScreen: boolean): Promise<any>;
async stopFrameCap(next: () => Promise<any>, driver: DriverClass): Promise<any>;
```
As with adding or overriding existing HTTP protocol commands, these methods are injected with
a `next` parameter and a `driver` parameter. The `driver` object represents the driver currently
owning the session, and calling `await next()` will execute/return the behavior that would execute
were the plugin not active (i.e., the driver's own handling of that method, or the chain of
commands executed with the same name by other active plugins).
Note that, currently, if a driver has BiDi proxying turned on, plugins will not be able to override
BiDi methods handled by the proxy. Also note that since these are custom BiDi commands, our module
name should include a vendor prefix (we chose `appium:`, but you can/should choose something that
makes sense for your extension).
#### Overloading Execute Script
An alternative to these ways of doing things is to overload a command which all WebDriver clients
have access to already: Execute Script. Make sure to read the section on [adding new
commands](./build-drivers.md#extend-the-existing-protocol-with-new-commands) in
the Building Drivers guide to understand the way this works in general. The way it works with
@@ -405,6 +461,11 @@ Overriding Execute Methods from drivers works as you'd expect: if your plugin de
Method with the same name as that of a driver, your command (in this case `plugMeIn`) will be
called first. You can choose to run the driver's original behaviour via `next` if you want.
### Emit BiDi Events
Your plugin can emit custom BiDi events in [exactly the same way as Appium
drivers](./build-drivers.md#emit-bidi-events).
### Build Appium Doctor checks
Your users can run `appium plugin doctor <pluginName>` to run installation and health checks. Visit
@@ -32,6 +32,7 @@ These clients are not maintained by the Appium team and can be used with other l
|[Nightwatch.js](https://nightwatchjs.org/guide/mobile-app-testing/introduction.html)|Node.js|
|[RobotFramework](https://github.com/serhatbolsu/robotframework-appiumlibrary)|DSL|
|[multicatch's appium-client](https://github.com/multicatch/appium-client)|Rust|
|[SwiftAppium](https://github.com/milcgroup/swiftappium)|Swift|
In general, any W3C WebDriver spec-compatible client will also integrate well with Appium, though
some Appium-specific commands may not be implemented in other clients.
@@ -44,8 +44,6 @@ These drivers are not maintained by the Appium team and can be used to target ad
|[Tizen](https://github.com/Samsung/appium-tizen-driver)|`--source=npm appium-tizen-driver`|Android|Native|Community / Samsung|
|[TizenTV](https://github.com/headspinio/appium-tizen-tv-driver)|`--source=npm appium-tizen-tv-driver`|Samsung TV|Web|HeadSpin|
|[Youi](https://github.com/YOU-i-Labs/appium-youiengine-driver)|`--source=npm appium-youiengine-driver`|iOS, Android, macOS, Linux, tvOS|Native|Community / You.i|
|[axe UiAutomator2](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-uiautomator2-driver`|Android|Native|[Deque](https://www.deque.com)|
|[axe XCUITest](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-xcuitest-driver`|iOS|Native|[Deque](https://www.deque.com)|
!!! note
+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
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
by the use of the `appium:eventTimings` capability (set it to `true` to log event
timings).
With this capability turned on, the `GET /session/:id` response (i.e., the
response to `driver.getSessionDetails()` or similar, depending on client) will
be decorated with an `events` property. This is the structure of that `events`
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
the response to `driver.logs.events` or similar, depending on client) will be
decorated with an `events` property. This is the structure of that `events`
property:
```
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
command, and so on.
You can only receive data about events that have happened when you make the
call to `/session/:id`, so the best time to get data about an entire session is
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
right before quitting it.
The Appium team maintains an event timings parser tool that can be used to
generate various kinds of reports from event timings output:
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
!!! note
In the past, events were available as a part of `GET /session/:id` response
## Add a custom event
!!! warning "TODO"
-5
View File
@@ -40,11 +40,6 @@ Android TV, Samsung), and more!
<img src="assets/images/sponsor-logo-browserstack-light.png#only-light" style="width: 220px;" />
</a>
</div>
<div class="homepageSponsor">
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner">
<img src="assets/images/sponsor-logo-sauce.png" style="width: 200px;" />
</a>
</div>
</div>
</div>
-1
View File
@@ -35,7 +35,6 @@ contributors!
<img src="../assets/images/sponsor-logo-browserstack-dark.png" width="250" alt="Browserstack"/>
</picture>
</a>
<a style="padding-left: 1em" href="https://saucelabs.com/resources/blog/appium-strategic-partner" target="_blank"><img src="../assets/images/sponsor-logo-sauce.png" width="250"/></a>
## Gold Sponsors
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
by the use of the `appium:eventTimings` capability (set it to `true` to log event
timings).
With this capability turned on, the `GET /session/:id` response (i.e., the
response to `driver.getSessionDetails()` or similar, depending on client) will
be decorated with an `events` property. This is the structure of that `events`
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
the response to `driver.logs.events` or similar, depending on client) will be
decorated with an `events` property. This is the structure of that `events`
property:
```
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
command, and so on.
You can only receive data about events that have happened when you make the
call to `/session/:id`, so the best time to get data about an entire session is
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
right before quitting it.
The Appium team maintains an event timings parser tool that can be used to
generate various kinds of reports from event timings output:
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
!!! note
In the past, events were available as a part of `GET /session/:id` response
## Add a custom event
!!! warning "TODO"
+65 -426
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,24 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
MAX_LOG_BODY_LENGTH,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
promoteAppiumOptions,
promoteAppiumOptionsForObject,
generateDriverLogPrefix,
} from '@appium/base-driver';
import AsyncLock from 'async-lock';
import {
parseCapsForInnerDriver,
pullSettings,
makeNonW3cCapsError,
isBroadcastIp,
fetchInterfaces,
V4_BROADCAST_IP,
validateFeatures,
} from './utils';
import {util, node, logger} from '@appium/support';
import {util} from '@appium/support';
import {getDefaultsForExtension} from './schema';
import {DRIVER_TYPE, BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
import WebSocket from 'ws';
import os from 'node:os';
const MIN_WS_CODE_VAL = 1000;
const MAX_WS_CODE_VAL = 1015;
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
import * as bidiCommands from './bidi-commands';
import * as inspectorCommands from './inspector-commands';
const desiredCapabilityConstraints = /** @type {const} */ ({
automationName: {
@@ -151,17 +144,6 @@ class AppiumDriver extends DriverCore {
})();
}
/**
* Retrieves logger instance for the current umbrella driver instance
*/
get log() {
if (!this._log) {
const instanceName = `${this.constructor.name}@${node.getObjectId(this).substring(0, 4)}`;
this._log = logger.getLogger(instanceName);
}
return this._log;
}
/**
* Cancel commands queueing for the umbrella Appium driver
*/
@@ -178,7 +160,6 @@ class AppiumDriver extends DriverCore {
return this.sessions[sessionId];
}
// eslint-disable-next-line require-await
async getStatus() {
// https://www.w3.org/TR/webdriver/#dfn-status
const statusObj = this._isShuttingDown
@@ -214,6 +195,11 @@ class AppiumDriver extends DriverCore {
}));
}
async getAppiumSessions () {
throw new errors.NotImplementedError('Not implemented yet. ' +
'Please check https://github.com/appium/appium/issues/20880 for more details.');
}
printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
this.log.info(
driverVersion
@@ -265,347 +251,6 @@ class AppiumDriver extends DriverCore {
}
}
/**
* Initialize a new bidi connection and set up handlers
* @param {import('ws').WebSocket} ws The websocket connection object
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
*/
onBidiConnection(ws, req) {
// TODO put bidi-related functionality into a mixin/helper class
// wrap all of the handler logic with exception handling so if something blows up we can log
// and close the websocket
try {
const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = this.initBidiSocket(
ws,
req,
);
this.initBidiSocketHandlers(
ws,
proxyClient,
send,
sendToProxy,
bidiHandlerDriver,
logSocketErr,
);
this.initBidiProxyHandlers(proxyClient, ws, send);
this.initBidiEventListeners(ws, bidiHandlerDriver, send);
} catch (err) {
this.log.error(err);
try {
ws.close();
} catch (ign) {}
}
}
/**
* Initialize a new bidi connection
* @param {import('ws').WebSocket} ws The websocket connection object
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
*/
initBidiSocket(ws, req) {
let outOfBandErrorPrefix = '';
const pathname = req.url;
if (!pathname) {
throw new Error('Invalid connection request: pathname missing from request');
}
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
const sessionMatch = bidiSessionRe.exec(pathname);
const noSessionMatch = bidiNoSessionRe.exec(pathname);
if (!sessionMatch && !noSessionMatch) {
throw new Error(
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
`Ignoring and will close the connection`,
);
}
// Let's figure out which driver is going to handle this socket connection. It's either going
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
// (if no session id is included in the bidi connection request)
/** @type {import('@appium/types').ExternalDriver | AppiumDriver} */
let bidiHandlerDriver;
/** @type {import('ws').WebSocket | null} */
let proxyClient = null;
if (sessionMatch) {
// If we found a session id, see if it matches an active session
const sessionId = sessionMatch[1];
bidiHandlerDriver = this.sessions[sessionId];
if (!bidiHandlerDriver) {
// The session ID sent in doesn't match an active session; just ignore this socket
// connection in that case
throw new Error(
`Got bidi connection request for session with id ${sessionId} which is closed ` +
`or does not exist. Closing the socket connection.`,
);
}
const driverName = bidiHandlerDriver.constructor.name;
outOfBandErrorPrefix = `[session ${sessionId}] `;
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
// store this socket connection for later removal on session deletion. theoretically there
// can be multiple sockets per session
if (!this.bidiSockets[sessionId]) {
this.bidiSockets[sessionId] = [];
}
this.bidiSockets[sessionId].push(ws);
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
if (bidiProxyUrl) {
try {
new URL(bidiProxyUrl);
} catch (ign) {
throw new Error(
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
`url ${bidiProxyUrl}, but this was not a valid url`,
);
}
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
proxyClient = new WebSocket(bidiProxyUrl);
this.bidiProxyClients[sessionId] = proxyClient;
}
} else {
this.log.info('Bidi websocket connection made to main server');
// no need to store the socket connection if it's to the main server since it will just
// stay open as long as the server itself is and will close when the server closes.
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
}
const logSocketErr = (/** @type {Error} */ err) =>
this.log.error(`${outOfBandErrorPrefix}${err}`);
// This is a function which wraps the 'send' method on a web socket for two reasons:
// 1. Make it async-await friendly
// 2. Do some logging if there's a send error
const sendFactory = (/** @type {import('ws').WebSocket} */ socket) => {
const socketSend = B.promisify(socket.send, {context: socket});
return async (/** @type {string|Buffer} */ data) => {
try {
await socketSend(data);
} catch (err) {
logSocketErr(err);
}
};
};
// Construct our send method for sending messages to the client
const send = sendFactory(ws);
// Construct a conditional send method for proxying messages from the client to an upstream
// bidi socket server (e.g. on a browser)
const sendToProxy = proxyClient ? sendFactory(proxyClient) : null;
return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
}
/**
* Set up handlers on upstream bidi socket we are proxying to/from
*
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from)
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
*/
initBidiProxyHandlers(proxyClient, ws, send) {
// Set up handlers for events that might come from the upstream bidi socket connection if
// we're in proxy mode
if (proxyClient) {
// Here we're receiving a message from the upstream socket server. We want to pass it on to
// the client
proxyClient.on('message', async (/** @type {Buffer|string} */ data) => {
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`<-- BIDI Received data from proxied bidi socket, sending to client. Data: ${logData}`,
);
await send(data);
});
// If the upstream socket server closes the connection, should close the connection to the
// client as well
proxyClient.on('close', (code, reason) => {
this.log.debug(
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
`Closing proxy connection to client`,
);
if (!_.isNumber(code)) {
code = parseInt(code, 10);
}
if (_.isNaN(code) || code < MIN_WS_CODE_VAL || code > MAX_WS_CODE_VAL) {
this.log.warn(
`Received code ${code} from upstream socket, but this is not a valid ` +
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
);
code = WS_FALLBACK_CODE;
}
ws.close(code, reason);
});
proxyClient.on('error', (err) => {
this.log.error(`Got error on upstream bidi socket connection: ${err}`);
});
}
}
/**
* Set up handlers on the bidi socket connection to the client
*
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from, if we're proxying)
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
* @param {((data: string | Buffer) => Promise<void>) | null} sendToProxy - a method used to send data to the
* upstream socket
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
* handling the bidi commands
* @param {(err: Error) => void} logSocketErr - a special prefixed logger
*/
initBidiSocketHandlers(ws, proxyClient, send, sendToProxy, bidiHandlerDriver, logSocketErr) {
ws.on('error', (err) => {
// Can't do much with random errors on the connection other than log them
logSocketErr(err);
});
ws.on('open', () => {
this.log.info('Bidi websocket connection is now open');
});
// Now set up handlers for the various events that might happen on the websocket connection
// coming from the client
// First is incoming messages from the client
ws.on('message', async (/** @type {Buffer} */ data) => {
if (proxyClient) {
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`--> BIDI Received data from client, sending to upstream bidi socket. Data: ${logData}`,
);
// if we're meant to proxy to an upstream bidi socket, just do that
// @ts-ignore sendToProxy is never null if proxyClient is truthy, but ts doesn't know
// that
await sendToProxy(data.toString('utf8'));
} else {
const res = await this.onBidiMessage(data, bidiHandlerDriver);
await send(JSON.stringify(res));
}
});
// Next consider if the client closes the socket connection on us
ws.on('close', (code, reason) => {
// Not sure if we need to do anything here if the client closes the websocket connection.
// Probably if a session was started via the socket, and the socket closes, we should end the
// associated session to free up resources. But otherwise, for sockets attached to existing
// sessions, doing nothing is probably right.
this.log.debug(`Bidi socket connection closed (code ${code}, reason: '${reason}')`);
// If we're proxying, might as well close the upstream connection and clean it up
if (proxyClient) {
this.log.debug('Also closing bidi proxy socket connection');
proxyClient.close(code, reason);
}
});
}
/**
* Set up bidi event listeners
*
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
* handling the bidi commands
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
*/
initBidiEventListeners(ws, bidiHandlerDriver, send) {
// If the driver emits a bidi event that should maybe get sent to the client, check to make
// sure the client is subscribed and then pass it on
let eventListener = async ({context, method, params}) => {
// if the driver didn't specify a context, use the empty context
if (!context) {
context = '';
}
if (!method || !params) {
throw new Error(
`Driver emitted a bidi event that was malformed. Require method and params keys ` +
`(with optional context). But instead received: ${JSON.stringify({
context,
method,
params,
})}`,
);
}
if (ws.readyState !== WebSocket.OPEN) {
// if the websocket is not still 'open', then we can ignore sending these events
if (ws.readyState > WebSocket.OPEN) {
// if the websocket is closed or closing, we can remove this listener as well to avoid
// leaks
bidiHandlerDriver.eventEmitter.removeListener(BIDI_EVENT_NAME, eventListener);
}
return;
}
if (bidiHandlerDriver.bidiEventSubs[method]?.includes(context)) {
this.log.info(
`<-- BIDI EVENT ${method} (context: '${context}', params: ${JSON.stringify(params)})`,
);
// now we can send the event onto the socket
const ev = {type: 'event', context, method, params};
await send(JSON.stringify(ev));
}
};
bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListener);
}
/**
* @param {Buffer} data
* @param {ExternalDriver | AppiumDriver} driver
*/
async onBidiMessage(data, driver) {
let resMessage, id, method, params;
const dataTruncated = _.truncate(data.toString(), {length: 100});
try {
try {
({id, method, params} = JSON.parse(data.toString('utf8')));
} catch (err) {
throw new errors.InvalidArgumentError(
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
);
}
driver.log.info(`--> BIDI message #${id}`);
if (!method) {
throw new errors.InvalidArgumentError(
`Missing method for BiDi operation in '${dataTruncated}'`,
);
}
if (!params) {
throw new errors.InvalidArgumentError(
`Missing params for BiDi operation in '${dataTruncated}`,
);
}
const result = await driver.executeBidiCommand(method, params);
// https://w3c.github.io/webdriver-bidi/#protocol-definition
resMessage = {
id,
type: 'success',
result,
};
} catch (err) {
resMessage = err.bidiErrObject(id);
}
driver.log.info(`<-- BIDI message #${id}`);
return resMessage;
}
/**
* Log a bidi server error
* @param {Error} err
*/
onBidiServerError(err) {
this.log.error(`Error from bidi websocket server: ${err}`);
}
/**
* Create a new session
* @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
@@ -695,6 +340,13 @@ class AppiumDriver extends DriverCore {
driverInstance.relaxedSecurityEnabled = true;
}
// We also want to assign any new Bidi Commands that the driver has specified, including all
// the standard bidi commands. But add a method existence guard since some old driver class
// instances might not have this method
if (_.isFunction(driverInstance.updateBidiCommands)) {
driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
}
if (!_.isEmpty(this.args.denyInsecure)) {
this.log.info('Explicitly preventing use of insecure features:');
this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
@@ -759,7 +411,7 @@ class AppiumDriver extends DriverCore {
);
// set the New Command Timeout for the inner driver
driverInstance.startNewCommandTimeout();
await driverInstance.startNewCommandTimeout();
// apply initial values to Appium settings (if provided)
if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
@@ -776,14 +428,13 @@ class AppiumDriver extends DriverCore {
await driverInstance.updateSettings(jwpSettings);
}
// if the user has asked for bidi support, and the driver says that it supports bidi,
// send our bidi url back to the user. The inner driver will need to have already saved any
// internal bidi urls it might want to proxy to, cause we are going to overwrite that
// information here!
if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
// if the user has asked for bidi support, send our bidi url back to the user. The inner
// driver will need to have already saved any internal bidi urls it might want to proxy to,
// cause we are going to overwrite that information here!
if (dCaps.webSocketUrl) {
const {address, port, basePath} = this.args;
const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
const host = determineBiDiHost(address);
const host = bidiCommands.determineBiDiHost(address);
const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
this.log.info(
`Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
@@ -922,32 +573,6 @@ class AppiumDriver extends DriverCore {
}
}
/**
* @param {string} sessionId
*/
cleanupBidiSockets(sessionId) {
// clean up any bidi sockets associated with session
if (this.bidiSockets[sessionId]) {
try {
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
for (const ws of this.bidiSockets[sessionId]) {
// 1001 means server is going away
ws.close(1001, 'Appium session is closing');
}
} catch {}
delete this.bidiSockets[sessionId];
const proxyClient = this.bidiProxyClients[sessionId];
if (proxyClient) {
this.log.debug(`Also closing proxy connection to upstream bidi server`);
try {
// 1000 means normal closure, which seems correct when Appium is acting as the client
proxyClient.close(1000);
} catch {}
delete this.bidiProxyClients[sessionId];
}
}
}
async deleteAllSessions(opts = {}) {
const sessionsCount = _.size(this.sessions);
if (0 === sessionsCount) {
@@ -975,12 +600,13 @@ class AppiumDriver extends DriverCore {
* Get the appropriate plugins for a session (or sessionless plugins)
*
* @param {?string} sessionId - the sessionId (or null) to use to find plugins
* @returns {Array} - array of plugin instances
* @returns {Array<import('@appium/types').Plugin>} - array of plugin instances
*/
pluginsForSession(sessionId = null) {
if (sessionId) {
if (!this.sessionPlugins[sessionId]) {
this.sessionPlugins[sessionId] = this.createPluginInstances();
const driverId = generateDriverLogPrefix(this.sessions[sessionId]);
this.sessionPlugins[sessionId] = this.createPluginInstances(driverId || null);
}
return this.sessionPlugins[sessionId];
}
@@ -1011,14 +637,19 @@ class AppiumDriver extends DriverCore {
/**
* Creates instances of all of the enabled Plugin classes
* @param {string|null} driverId - ID to use for linking a driver to a plugin in logs
* @returns {Plugin[]}
*/
createPluginInstances() {
createPluginInstances(driverId = null) {
/** @type {Plugin[]} */
const pluginInstances = [];
for (const [PluginClass, name] of this.pluginClasses.entries()) {
const cliArgs = this.getCliArgsForPlugin(name);
const plugin = new PluginClass(name, cliArgs);
const plugin = new PluginClass(name, cliArgs, driverId);
if (_.isFunction(/** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands)) {
// some old plugin classes don't have `updateBidiCommands`
/** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands(PluginClass.newBidiCommands ?? {});
}
pluginInstances.push(plugin);
}
return pluginInstances;
@@ -1102,7 +733,9 @@ class AppiumDriver extends DriverCore {
// if we're running with plugins, make sure we log that the default behavior is actually
// happening so we can tell when the plugin call chain is unwrapping to the default behavior
// if that's what happens
plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
if (plugins.length) {
this.log.info(`Executing default handling behavior for command '${cmd}'`);
}
// if we make it here, we know that the default behavior is handled
cmdHandledBy.default = true;
@@ -1177,6 +810,12 @@ class AppiumDriver extends DriverCore {
`to session ID ${sessionId}`,
);
this.sessionPlugins[sessionId] = this.sessionlessPlugins;
for (const p of /** @type {(Plugin & ExtensionCore)[]} */(this.sessionPlugins[sessionId])) {
if (_.isFunction(p.updateLogPrefix)) {
// some old plugin classes don't have `updateLogPrefix` yet
p.updateLogPrefix(`${generateDriverLogPrefix(p)} <${generateDriverLogPrefix(this.sessions[sessionId])}>`);
}
}
this.sessionlessPlugins = [];
}
@@ -1184,8 +823,9 @@ class AppiumDriver extends DriverCore {
}
wrapCommandWithPlugins({driver, cmd, args, next, cmdHandledBy, plugins}) {
plugins.length &&
if (plugins.length) {
this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
}
// now we can go through each plugin and wrap `next` around its own handler, passing the *old*
// next in so that it can call it if it wants to
@@ -1276,31 +916,29 @@ class AppiumDriver extends DriverCore {
const dstSession = this.sessions[sessionId];
return dstSession && dstSession.canProxy(sessionId);
}
}
// help decide which commands should be proxied to sub-drivers and which
// should be handled by this, our umbrella driver
function isAppiumDriverCommand(cmd) {
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
onBidiConnection = bidiCommands.onBidiConnection;
onBidiMessage = bidiCommands.onBidiMessage;
onBidiServerError = bidiCommands.onBidiServerError;
cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
listCommands = inspectorCommands.listCommands;
listExtensions = inspectorCommands.listExtensions;
}
/**
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
* to create connections. Thus we prefer a hostname if such
* address is provided or the actual address of a non-local interface,
* in case the host only has one such interface.
*
* @param {string} address
* @returns {string}
* Help decide which commands should be proxied to sub-drivers and which
* should be handled by this, our umbrella driver
* @param {string} cmd
* @returns {boolean}
*/
function determineBiDiHost(address) {
if (!isBroadcastIp(address)) {
return address;
}
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
.filter((iface) => !iface.internal);
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
function isAppiumDriverCommand(cmd) {
return !isSessionCommand(cmd)
|| _.includes([
DELETE_SESSION_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
], cmd);
}
/**
@@ -1338,6 +976,7 @@ export {AppiumDriver};
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
* @typedef {import('@appium/types').PluginClass} PluginClass
* @typedef {import('@appium/types').Plugin} Plugin
* @typedef {import('@appium/base-driver').ExtensionCore} ExtensionCore
* @typedef {import('@appium/types').DriverClass<import('@appium/types').Driver>} DriverClass
*/
+543
View File
@@ -0,0 +1,543 @@
import _ from 'lodash';
import B from 'bluebird';
import {
errors,
ExtensionCore,
} from '@appium/base-driver';
import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
import WebSocket from 'ws';
import os from 'node:os';
import {
isBroadcastIp,
fetchInterfaces,
V4_BROADCAST_IP,
} from './utils';
import type {IncomingMessage} from 'node:http';
import type {AppiumDriver} from './appium';
import type {
ErrorBiDiCommandResponse,
SuccessBiDiCommandResponse,
ExternalDriver,
StringRecord,
Plugin,
BiDiResultData
} from '@appium/types';
type ExtensionPlugin = Plugin & ExtensionCore
type AnyDriver = ExternalDriver | AppiumDriver;
type SendData = (data: string | Buffer) => Promise<void>;
type LogSocketError = (err: Error) => void;
interface InitBiDiSocketResult {
bidiHandlerDriver: AnyDriver;
bidiHandlerPlugins: ExtensionPlugin[];
proxyClient: WebSocket | null;
send: SendData;
sendToProxy: SendData | null;
logSocketErr: LogSocketError;
}
const MIN_WS_CODE_VAL = 1000;
const MAX_WS_CODE_VAL = 1015;
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
const BIDI_EVENTS_MAP: WeakMap<AnyDriver, Record<string, number>> = new WeakMap();
const MAX_LOGGED_DATA_LENGTH = 300;
/**
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
* to create connections. Thus we prefer a hostname if such
* address is provided or the actual address of a non-local interface,
* in case the host only has one such interface.
*
* @param address
*/
export function determineBiDiHost(address: string): string {
if (!isBroadcastIp(address)) {
return address;
}
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
.filter((iface) => !iface.internal);
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
}
/**
* Initialize a new bidi connection and set up handlers
* @param ws The websocket connection object
* @param req The connection pathname, which might include the session id
*/
export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
try {
const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
const {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
ws,
req,
);
const initBidiSocketHandlersFunc: OmitThisParameter<typeof initBidiSocketHandlers> = initBidiSocketHandlers
.bind(this);
initBidiSocketHandlersFunc(
ws,
proxyClient,
send,
sendToProxy,
bidiHandlerDriver,
bidiHandlerPlugins,
logSocketErr,
);
if (proxyClient) {
const initBidiProxyHandlersFunc: OmitThisParameter<typeof initBidiProxyHandlers> = initBidiProxyHandlers
.bind(bidiHandlerDriver);
initBidiProxyHandlersFunc(proxyClient, ws, send);
}
const initBidiEventListenersFunc: OmitThisParameter<typeof initBidiEventListeners> = initBidiEventListeners
.bind(this);
initBidiEventListenersFunc(ws, bidiHandlerDriver, bidiHandlerPlugins, send);
} catch (err) {
this.log.error(err);
try {
ws.close();
} catch {}
}
}
function wrapCommandWithPlugins(driver: ExtensionCore, plugins: ExtensionCore[], method: string, params: StringRecord): () => Promise<BiDiResultData> {
const [moduleName, methodName] = method.split('.');
let next = async () => await driver.executeBidiCommand(method, params);
for (const plugin of plugins.filter((p) => p.doesBidiCommandExist(moduleName, methodName))) {
next = ((_next) => async () => await plugin.executeBidiCommand(method, params, _next, driver))(next);
}
return next;
}
/**
* @param data
* @param driver
* @param plugins
*/
export async function onBidiMessage(
this: AppiumDriver,
data: Buffer,
driver: AnyDriver,
plugins: ExtensionPlugin[]
): Promise<SuccessBiDiCommandResponse | ErrorBiDiCommandResponse> {
let resMessage: SuccessBiDiCommandResponse | ErrorBiDiCommandResponse;
let id: number = 0;
const driverLog = driver.log;
const dataTruncated = _.truncate(data.toString(), {length: MAX_LOGGED_DATA_LENGTH});
try {
let method: string;
let params: StringRecord;
try {
({id, method, params} = JSON.parse(data.toString('utf8')));
} catch (err) {
throw new errors.InvalidArgumentError(
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
);
}
driverLog.info(`--> BIDI message #${id}`);
if (!method) {
throw new errors.InvalidArgumentError(
`Missing method for BiDi operation in '${dataTruncated}'`,
);
}
if (!params) {
throw new errors.InvalidArgumentError(
`Missing params for BiDi operation in '${dataTruncated}`,
);
}
const executeWrappedCommand = wrapCommandWithPlugins(driver as ExtensionCore, plugins, method, params);
const result = await executeWrappedCommand();
resMessage = {
id,
type: 'success',
result,
};
} catch (err) {
resMessage = _.has(err, 'bidiErrObject')
? err.bidiErrObject(id)
: {
id,
type: 'error',
error: errors.UnknownError.error(),
message: (err as Error).message,
stacktrace: (err as Error).stack,
};
}
driverLog.info(`<-- BIDI message #${id}`);
return resMessage;
}
/**
* Log a bidi server error
* @param err
*/
export function onBidiServerError(this: AppiumDriver, err: Error): void {
this.log.warn(`Error from bidi websocket server: ${err}`);
}
/**
* Clean up any bidi sockets associated with session
*
* @param sessionId
*/
export function cleanupBidiSockets(this: AppiumDriver, sessionId: string): void {
if (!this.bidiSockets[sessionId]) {
return;
}
try {
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
for (const ws of this.bidiSockets[sessionId]) {
// 1001 means server is going away
ws.close(1001, 'Appium session is closing');
}
} catch {}
delete this.bidiSockets[sessionId];
const proxyClient = this.bidiProxyClients[sessionId];
if (!proxyClient) {
return;
}
this.log.debug(`Also closing proxy connection to upstream bidi server`);
try {
// 1000 means normal closure, which seems correct when Appium is acting as the client
proxyClient.close(1000);
} catch {}
delete this.bidiProxyClients[sessionId];
}
// #region Private functions
/**
* Initialize a new bidi connection
* @param ws The websocket connection object
* @param req The connection pathname, which might include the session id
*/
function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): InitBiDiSocketResult {
const pathname = req.url;
if (!pathname) {
throw new Error('Invalid connection request: pathname missing from request');
}
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
const sessionMatch = bidiSessionRe.exec(pathname);
const noSessionMatch = bidiNoSessionRe.exec(pathname);
if (!sessionMatch && !noSessionMatch) {
throw new Error(
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
`Ignoring and will close the connection`,
);
}
// Let's figure out which driver is going to handle this socket connection. It's either going
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
// (if no session id is included in the bidi connection request)
let bidiHandlerDriver: AnyDriver;
let proxyClient: WebSocket | null = null;
const bidiHandlerPlugins: ExtensionPlugin[] = [];
if (sessionMatch) {
// If we found a session id, see if it matches an active session
const sessionId = sessionMatch[1];
bidiHandlerDriver = this.sessions[sessionId];
if (!bidiHandlerDriver) {
// The session ID sent in doesn't match an active session; just ignore this socket
// connection in that case
throw new Error(
`Got bidi connection request for session with id ${sessionId} which is closed ` +
`or does not exist. Closing the socket connection.`,
);
}
const driverName = bidiHandlerDriver.constructor.name;
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
// store this socket connection for later removal on session deletion. theoretically there
// can be multiple sockets per session
if (!this.bidiSockets[sessionId]) {
this.bidiSockets[sessionId] = [];
}
this.bidiSockets[sessionId].push(ws);
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
if (bidiProxyUrl) {
try {
new URL(bidiProxyUrl);
} catch {
throw new Error(
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
`url ${bidiProxyUrl}, but this was not a valid url`,
);
}
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}. ` +
`Plugins will not handle bidi commands`);
proxyClient = new WebSocket(bidiProxyUrl);
this.bidiProxyClients[sessionId] = proxyClient;
} else {
bidiHandlerPlugins.push(...this.pluginsForSession(sessionId) as ExtensionPlugin[]);
}
} else {
this.log.info('Bidi websocket connection made to main server');
// no need to store the socket connection if it's to the main server since it will just
// stay open as long as the server itself is and will close when the server closes.
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
bidiHandlerPlugins.push(...this.pluginsForSession() as ExtensionPlugin[]);
}
const driverLog = bidiHandlerDriver.log;
const logSocketErr: LogSocketError = (err: Error) => {
driverLog.warn(err.message);
};
// This is a function which wraps the 'send' method on a web socket for two reasons:
// 1. Make it async-await friendly
// 2. Do some logging if there's a send error
const sendFactory = (socket: WebSocket) => {
const socketSend = B.promisify(socket.send, {context: socket});
return async (data: string | Buffer) => {
try {
await assertIsOpen(socket);
await socketSend(data);
} catch (err) {
logSocketErr(err);
}
};
};
// Construct our send method for sending messages to the client
const send: SendData = sendFactory(ws);
// Construct a conditional send method for proxying messages from the client to an upstream
// bidi socket server (e.g. on a browser)
const sendToProxy: SendData | null = proxyClient ? sendFactory(proxyClient) : null;
return {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr};
}
/**
* Set up handlers on upstream bidi socket we are proxying to/from
*
* @param proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from)
* @param ws - the websocket connection to/from the client
* @param send - a method used to send data to the
* client
*/
function initBidiProxyHandlers(
this: AnyDriver,
proxyClient: WebSocket,
ws: WebSocket,
send: SendData,
): void {
// Set up handlers for events that might come from the upstream bidi socket connection if
// we're in proxy mode
const driverLog = this.log;
// Here we're receiving a message from the upstream socket server. We want to pass it on to
// the client
proxyClient.on('message', send);
// If the upstream socket server closes the connection, should close the connection to the
// client as well
proxyClient.on('close', (code, reason) => {
driverLog.debug(
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
`Closing proxy connection to client`,
);
const intCode: number = _.isNumber(code) ? (code as number) : parseInt(code, 10);
if (_.isNaN(intCode) || intCode < MIN_WS_CODE_VAL || intCode > MAX_WS_CODE_VAL) {
driverLog.warn(
`Received code ${code} from upstream socket, but this is not a valid ` +
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
);
code = WS_FALLBACK_CODE;
}
ws.close(code, reason);
});
proxyClient.on('error', (err) => {
driverLog.warn(`Got error on upstream bidi socket connection: ${err.message}`);
});
}
/**
* Set up handlers on the bidi socket connection to the client
*
* @param ws - the websocket connection to/from the client
* @param proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from, if we're proxying)
* @param send - a method used to send data to the
* client
* @param sendToProxy - a method used to send data to the
* upstream socket
* @param bidiHandlerDriver - the driver
* handling the bidi commands
* @param bidiHandlerPlugins - plugins that might also handle bidi commands
* @param logSocketErr - a special prefixed logger
*/
function initBidiSocketHandlers(
this: AppiumDriver,
ws: WebSocket,
proxyClient: WebSocket | null,
send: SendData,
sendToProxy: SendData | null,
bidiHandlerDriver: AnyDriver,
bidiHandlerPlugins: ExtensionPlugin[],
logSocketErr: LogSocketError,
): void {
const driverLog = bidiHandlerDriver.log;
// Can't do much with random errors on the connection other than log them
ws.on('error', logSocketErr);
ws.on('open', () => {
driverLog.info('BiDi websocket connection is now open');
});
// Now set up handlers for the various events that might happen on the websocket connection
// coming from the client
// First is incoming messages from the client
ws.on('message', async (data: Buffer) => {
if (proxyClient && sendToProxy) {
// if we're meant to proxy to an upstream bidi socket, just do that
// TODO trying to determine how this proxying behaviour would interface with plugins is too
// complex for now, so just ignore plugins in this case
await sendToProxy(data.toString('utf8'));
} else {
const res = await this.onBidiMessage(data, bidiHandlerDriver, bidiHandlerPlugins);
await send(JSON.stringify(res));
}
});
// Next consider if the client closes the socket connection on us
ws.on('close', (code, reason) => {
// Not sure if we need to do anything here if the client closes the websocket connection.
// Probably if a session was started via the socket, and the socket closes, we should end the
// associated session to free up resources. But otherwise, for sockets attached to existing
// sessions, doing nothing is probably right.
driverLog.debug(`BiDi socket connection closed (code ${code}, reason: '${reason}')`);
// If we're proxying, might as well close the upstream connection and clean it up
if (proxyClient) {
driverLog.debug('Also closing BiDi proxy socket connection');
proxyClient.close(code, reason);
}
const eventLogCounts = BIDI_EVENTS_MAP.get(bidiHandlerDriver);
if (!_.isEmpty(eventLogCounts)) {
driverLog.debug(`BiDi events statistics: ${JSON.stringify(eventLogCounts, null, 2)}`);
}
});
}
/**
* Set up bidi event listeners
*
* @param ws - the websocket connection to/from the client
* @param bidiHandlerDriver - the driver
* handling the bidi commands
* @param send - a method used to send data to the
* client
*/
function initBidiEventListeners(
this: AppiumDriver,
ws: WebSocket,
bidiHandlerDriver: AnyDriver,
bidiHandlerPlugins: ExtensionPlugin[],
send: SendData,
): void {
// If the driver emits a bidi event that should maybe get sent to the client, check to make
// sure the client is subscribed and then pass it on
const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
const eventListenerFactory = (extType: 'driver'|'plugin', ext: ExtensionCore) => {
const eventListener = async ({context, method, params = {}}) => {
// if the driver didn't specify a context, use the empty context
if (!context) {
context = '';
}
if (!method || !params) {
ext.log?.warn( // some old plugins might not have the `log` property
`${_.capitalize(extType)} emitted a bidi event that was malformed. Require method and params keys ` +
`(with optional context). But instead received: ${_.truncate(JSON.stringify({
context,
method,
params,
}), {length: MAX_LOGGED_DATA_LENGTH})}`,
);
return;
}
if (ws.readyState !== WebSocket.OPEN) {
// if the websocket is not still 'open', then we can ignore sending these events
if (ws.readyState > WebSocket.OPEN) {
// if the websocket is closed or closing, we can remove this listener as well to avoid
// leaks. Some old plugin classes might not have the `eventEmitter` property, so use an
// existence guard for now.
ext.eventEmitter?.removeListener(BIDI_EVENT_NAME, eventListener);
}
return;
}
const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
if (_.isArray(eventSubs) && eventSubs.includes(context)) {
if (method in eventLogCounts) {
++eventLogCounts[method];
} else {
ext.log?.info( // some old plugins might not have the `log` property
`<-- BIDI EVENT ${method} (context: '${context}', ` +
`params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
`All further similar events won't be logged.`,
);
eventLogCounts[method] = 1;
}
// now we can send the event onto the socket
const ev = {type: 'event', context, method, params};
await send(JSON.stringify(ev));
}
};
return eventListener;
};
bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListenerFactory('driver', bidiHandlerDriver as ExtensionCore));
for (const plugin of bidiHandlerPlugins) {
// some old plugins might not have the eventEmitter property
plugin.eventEmitter?.on(BIDI_EVENT_NAME, eventListenerFactory('plugin', plugin));
}
}
async function assertIsOpen(
ws: WebSocket,
timeoutMs: number = 5000,
): Promise<WebSocket> {
if (ws.readyState === ws.OPEN) {
return ws;
}
if (ws.readyState > ws.OPEN) {
throw new Error(`The BiDi web socket at ${ws.url} is not open`);
}
let errorListener;
let openListener;
// The socket is in CONNECTING state. Wait up to `timeoutMs` until it is open
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(
new Error(
`The BiDi web socket at ${ws.url} did not ` +
`open after ${timeoutMs}ms timeout`
)
), timeoutMs);
ws.once('error', reject);
errorListener = reject;
ws.once('open', resolve);
openListener = resolve;
});
} finally {
if (errorListener) {
ws.off('error', errorListener);
}
if (openListener) {
ws.off('open', openListener);
}
}
return ws;
}
// #endregion
+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}`,
+190
View File
@@ -0,0 +1,190 @@
import _ from 'lodash';
import {
METHOD_MAP as BASE_METHOD_MAP,
BIDI_COMMANDS as BASE_BIDI_COMMANDS,
} from '@appium/base-driver';
import type {
ListCommandsResponse,
MethodMap,
BidiModuleMap,
DriverClass,
PluginClass,
ListExtensionsResponse,
PayloadParams,
RestCommandItemParam,
RestMethodsToCommandsMap,
BiDiCommandsMap,
BidiMethodParams,
BiDiCommandItemParam,
BiDiCommandNamesToInfosMap,
ExecuteMethodMap,
} from '@appium/types';
import type { AppiumDriver } from './appium';
export async function listCommands(this: AppiumDriver, sessionId?: string): Promise<ListCommandsResponse> {
let driverRestMethodMap: MethodMap<any> = {};
let driverBiDiCommands: BidiModuleMap = {};
let pluginRestMethodMaps: Record<string, MethodMap<any>> = {};
let pluginBiDiCommands: Record<string, BidiModuleMap> = {};
if (sessionId) {
const driverClass = this.driverForSession(sessionId)?.constructor as (DriverClass | undefined);
driverRestMethodMap = driverClass?.newMethodMap ?? {};
driverBiDiCommands = driverClass?.newBidiCommands ?? {};
const pluginClasses = this.pluginsForSession(sessionId)
.map((p) => p.constructor as PluginClass);
pluginRestMethodMaps = _.fromPairs(pluginClasses.map((c) => [c.name, c.newMethodMap ?? {}]));
pluginBiDiCommands = _.fromPairs(pluginClasses.map((c) => [c.name, c.newBidiCommands ?? {}]));
}
return {
rest: {
base: methodMapToRestCommandsInfo(BASE_METHOD_MAP),
driver: methodMapToRestCommandsInfo(driverRestMethodMap),
plugins: pluginRestMethodMaps ? _.mapValues(pluginRestMethodMaps, methodMapToRestCommandsInfo) : undefined,
},
bidi: toBiDiCommandsMap(BASE_BIDI_COMMANDS, driverBiDiCommands, pluginBiDiCommands),
};
}
export async function listExtensions(this: AppiumDriver, sessionId?: string): Promise<ListExtensionsResponse> {
let driverExecuteMethodMap: ExecuteMethodMap<any> = {};
let pluginExecuteMethodMaps: Record<string, ExecuteMethodMap<any>> = {};
if (sessionId) {
const driverClass = this.driverForSession(sessionId)?.constructor as (DriverClass | undefined);
driverExecuteMethodMap = driverClass?.executeMethodMap ?? {};
const pluginClasses = this.pluginsForSession(sessionId)
.map((p) => p.constructor as PluginClass);
pluginExecuteMethodMaps = _.fromPairs(pluginClasses.map((c) => [c.name, c.executeMethodMap ?? {}]));
}
return {
rest: {
driver: executeMethodMapToCommandsInfo(driverExecuteMethodMap),
plugins: pluginExecuteMethodMaps ? _.mapValues(pluginExecuteMethodMaps, executeMethodMapToCommandsInfo) : undefined,
},
};
}
function toRestCommandParams(params: PayloadParams | undefined): RestCommandItemParam[] | undefined {
if (!params) {
return;
}
const toRestCommandItemParam = (x: any, isRequired: boolean): RestCommandItemParam | undefined => {
const isNameAnArray = _.isArray(x);
const name = isNameAnArray ? x[0] : x;
if (!_.isString(name)) {
return;
}
// If parameter names are arrays then this means
// either of them is required.
// Not sure we could reflect that in here.
const required = isRequired && !isNameAnArray;
return {
name,
required,
};
};
const requiredParams: RestCommandItemParam[] = (params.required ?? [])
.map((name: any) => toRestCommandItemParam(name, true))
.filter((x) => !_.isUndefined(x));
const optionalParams: RestCommandItemParam[] = (params.optional ?? [])
.map((name: any) => toRestCommandItemParam(name, false))
.filter((x) => !_.isUndefined(x));
return requiredParams.length || optionalParams.length
? [...requiredParams, ...optionalParams]
: undefined;
}
function methodMapToRestCommandsInfo (mm: MethodMap<any>): Record<string, RestMethodsToCommandsMap> {
const res: Record<string, RestMethodsToCommandsMap> = {};
for (const [uriPath, methods] of _.toPairs(mm)) {
const methodsMap = {};
for (const [method, spec] of _.toPairs(methods)) {
methodsMap[method] = {
command: spec.command,
deprecated: spec.deprecated,
info: spec.info,
params: toRestCommandParams(spec.payloadParams),
};
}
res[uriPath] = methodsMap;
}
return res;
}
function executeMethodMapToCommandsInfo(emm: ExecuteMethodMap<any>): RestMethodsToCommandsMap {
const result: RestMethodsToCommandsMap = {};
for (const [name, info] of _.toPairs(emm)) {
result[name] = {
command: info.command,
deprecated: info.deprecated,
info: info.info,
params: toRestCommandParams(info.params),
};
}
return result;
}
function toBiDiCommandsMap(
baseModuleMap: BidiModuleMap,
driverModuleMap: BidiModuleMap,
pluginModuleMaps: Record<string, BidiModuleMap>
): BiDiCommandsMap {
const toBiDiCommandParams = (params: BidiMethodParams | undefined): BiDiCommandItemParam[] | undefined => {
if (!params) {
return;
}
const toBiDiCommandItemParam = (x: any, isRequired: boolean): BiDiCommandItemParam | undefined => {
const isNameAnArray = _.isArray(x);
const name = isNameAnArray ? x[0] : x;
if (!_.isString(name)) {
return;
}
// If parameter names are arrays then this means
// either of them is required.
// Not sure we could reflect that in here.
const required = isRequired && !isNameAnArray;
return {
name,
required,
};
};
const requiredParams: BiDiCommandItemParam[] = (params.required ?? [])
.map((name) => toBiDiCommandItemParam(name, true))
.filter((x) => !_.isUndefined(x));
const optionalParams: BiDiCommandItemParam[] = (params.optional ?? [])
.map((name) => toBiDiCommandItemParam(name, false))
.filter((x) => !_.isUndefined(x));
return requiredParams.length || optionalParams.length
? [...requiredParams, ...optionalParams]
: undefined;
};
const moduleMapToBiDiCommandsInfo = (mm: BidiModuleMap): Record<string, BiDiCommandNamesToInfosMap> => {
const res: Record<string, BiDiCommandNamesToInfosMap> = {};
for (const [domain, commands] of _.toPairs(mm)) {
const commandsMap = {};
for (const [name, spec] of _.toPairs(commands)) {
commandsMap[name] = {
command: spec.command,
deprecated: spec.deprecated,
info: spec.info,
params: toBiDiCommandParams(spec.params),
};
}
res[domain] = commandsMap;
}
return res;
};
return {
base: moduleMapToBiDiCommandsInfo(baseModuleMap),
driver: moduleMapToBiDiCommandsInfo(driverModuleMap),
plugins: pluginModuleMaps ? _.mapValues(pluginModuleMaps, moduleMapToBiDiCommandsInfo) : undefined,
};
}
+4 -2
View File
@@ -397,8 +397,10 @@ async function main(args) {
extraMethodMap,
cliArgs: parsedArgs,
};
if (parsedArgs.keepAliveTimeout) {
serverOpts.keepAliveTimeout = parsedArgs.keepAliveTimeout * 1000;
for (const timeoutArgName of ['keepAliveTimeout', 'requestTimeout']) {
if (_.isInteger(parsedArgs[timeoutArgName])) {
serverOpts[timeoutArgName] = parsedArgs[timeoutArgName] * 1000;
}
}
let server;
const bidiServer = new WebSocketServer({noServer: true});
+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;
}
};
+11 -17
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.13.1",
"version": "2.16.0",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,19 +60,14 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.13.1",
"@appium/base-plugin": "^2.2.50",
"@appium/docutils": "^1.0.26",
"@appium/base-driver": "^9.16.0",
"@appium/base-plugin": "^2.3.1",
"@appium/docutils": "^1.0.30",
"@appium/logger": "^1.6.1",
"@appium/schema": "^0.7.0",
"@appium/support": "^6.0.0",
"@appium/types": "^0.22.3",
"@appium/schema": "^0.8.0",
"@appium/support": "^6.0.4",
"@appium/types": "^0.25.0",
"@sidvind/better-ajv-errors": "3.0.1",
"@types/argparse": "2.0.17",
"@types/bluebird": "3.5.42",
"@types/semver": "7.5.8",
"@types/teen_process": "2.0.4",
"@types/wrap-ansi": "3.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"argparse": "2.0.1",
@@ -80,21 +75,20 @@
"asyncbox": "3.0.0",
"axios": "1.7.9",
"bluebird": "3.7.2",
"cross-env": "7.0.3",
"lilconfig": "3.1.3",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
"ora": "5.4.1",
"package-changed": "3.0.0",
"resolve-from": "5.0.0",
"semver": "7.6.3",
"semver": "7.7.1",
"source-map-support": "0.5.21",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"teen_process": "2.2.3",
"type-fest": "4.35.0",
"winston": "3.17.0",
"wrap-ansi": "7.0.0",
"ws": "8.18.0",
"yaml": "2.6.1"
"yaml": "2.7.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "8.40.6"
"webdriverio": "9.9.3"
}
}
@@ -85,10 +85,10 @@ async function init() {
}
try {
({env, util, logger} = require('@appium/support'));
// @ts-ignore
// @ts-ignore This is OK
({runExtensionCommand} = require('../build/lib/cli/extension'));
({DRIVER_TYPE, PLUGIN_TYPE} = require('../build/lib/constants'));
// @ts-ignore
// @ts-ignore This is OK
({loadExtensions} = require('../build/lib/extension'));
logger.getLogger('Appium').level = 'error';
@@ -119,7 +119,7 @@ async function main() {
if (!driverEnv && !pluginEnv) {
spinner.succeed(
wrap(`No drivers or plugins to automatically install.
wrap(`No drivers or plugins to automatically install.
If desired, provide arguments with comma-separated values "--drivers=<known_driver>[,known_driver...]" and/or "--plugins=<known_plugin>[,known_plugin...]" to the "npm install appium" command. The specified extensions will be installed automatically with Appium. Note: to see the list of known extensions, run "appium <driver|plugin> list".`)
);
return;
@@ -193,7 +193,7 @@ async function checkAndInstallExtension({
spinner,
}) {
const extList = await runExtensionCommand(
// @ts-ignore
// @ts-ignore This is OK
{
appiumHome,
subcommand: type,
@@ -209,7 +209,7 @@ async function checkAndInstallExtension({
}
spinner.start(`Installing ${type} "${ext}"...`);
await runExtensionCommand(
// @ts-ignore
// @ts-ignore this is OK
{
subcommand: type,
appiumHome,
+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';
+100 -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();
@@ -206,6 +208,77 @@ describe('FakeDriver via HTTP', function () {
});
});
describe('inspector commands', function () {
withServer();
let driver;
beforeEach(async function () {
driver = await wdio({...wdOpts, capabilities: caps});
});
afterEach(async function () {
if (driver) {
await driver.deleteSession();
driver = null;
}
});
it('should list available driver commands', async function () {
driver.addCommand(
'listCommands',
async () => (await axios.get(
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/commands`
)).data.value
);
const commands = await driver.listCommands();
JSON.stringify(commands.rest.base['/session/:sessionId/frame'])
.should.eql(JSON.stringify({POST: {command: 'setFrame', params: [
{name: 'id', required: true}
]}}));
_.size(commands.rest.driver).should.be.greaterThan(1);
JSON.stringify(commands.bidi.base.session.subscribe).should.eql(
JSON.stringify({
command: 'bidiSubscribe',
'params': [
{
name: 'events',
required: true
},
{
name: 'contexts',
required: false
}
]
})
);
_.size(commands.bidi.base).should.be.greaterThan(1);
_.size(commands.bidi.driver).should.be.greaterThan(0);
});
it('should list available driver extensions', async function () {
driver.addCommand(
'listExtensions',
async () => (await axios.get(
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/extensions`
)).data.value
);
const extensions = await driver.listExtensions();
JSON.stringify(extensions.rest.driver['fake: setThing']).should.eql(
JSON.stringify({
command: 'setFakeThing',
params: [{
name: 'thing',
required: true
}]
})
);
_.size(extensions.rest.driver).should.be.greaterThan(1);
});
});
describe('session handling', function () {
withServer();
@@ -251,8 +324,6 @@ describe('FakeDriver via HTTP', function () {
await B.delay(250);
await driver.getPageSource().should.eventually.be.rejectedWith(/terminated/);
await driver.getSessions().should.eventually.be.empty;
});
it('should not allow umbrella commands to prevent newCommandTimeout on inner driver', async function () {
@@ -264,6 +335,10 @@ describe('FakeDriver via HTTP', function () {
);
let driver = await wdio({...wdOpts, capabilities: localCaps});
should.exist(driver.sessionId);
driver.addCommand(
'getSessions',
async () => (await axios.get(`${testServerBaseUrl}/sessions`)).data.value
);
// get the session list 6 times over 300ms. each request will be below the new command
// timeout but since they are not received by the driver the session should still time out
@@ -506,51 +581,12 @@ describe('FakeDriver via HTTP', function () {
await driver.deleteSession();
}
});
it.skip('should log a single deprecation warning if a deprecated method is used and not overridden by a newMethodMap', async function () {
let driver = await wdio({...wdOpts, capabilities: caps});
try {
driver.addCommand(
'deprecated',
command('POST', '/session/:sessionId/deprecated', {
command: 'deprecated',
description: 'Call a deprecated command',
parameters: [],
ref: '',
}),
);
driver.addCommand(
'doubleClick',
command('POST', '/session/:sessionId/doubleclick', {
command: 'doubleClick',
description: 'Global double click',
parameters: [],
ref: '',
}),
);
await driver
.executeScript('fake: getDeprecatedCommandsCalled', [])
.should.eventually.eql([]);
await driver.deprecated();
await driver.deprecated();
await driver.shake();
// this call should not trigger a deprecation even though deprecated by appium because it's
// overridden as not deprecated by fake driver
await driver.doubleClick();
await driver
.executeScript('fake: getDeprecatedCommandsCalled', [])
.should.eventually.eql(['callDeprecatedCommand', 'mobileShake']);
} finally {
await driver.deleteSession();
}
});
});
describe('Bidi protocol', function () {
withServer();
const capabilities = {...caps, webSocketUrl: true, 'appium:runClock': true};
/** @type import('webdriverio').Browser **/
let driver;
beforeEach(async function () {
@@ -581,7 +617,7 @@ describe('FakeDriver via HTTP', function () {
it('should be able to subscribe and unsubscribe to bidi events', async function () {
let collectedEvents = [];
// asyncrhonously start our listener
driver.on('clock.currentTime', ({time}) => {
driver.on('appium:clock.currentTime', ({time}) => {
collectedEvents.push(time);
});
@@ -591,16 +627,24 @@ describe('FakeDriver via HTTP', function () {
collectedEvents.should.be.empty;
// now subscribe and wait and assert that some events have been collected
await driver.sessionSubscribe({events: ['clock.currentTime']});
await driver.sessionSubscribe({events: ['appium:clock.currentTime']});
await B.delay(750);
collectedEvents.should.not.be.empty;
// finally unsubscribe and wait and assert that some events have been collected
await driver.sessionUnsubscribe({events: ['clock.currentTime']});
await driver.sessionUnsubscribe({events: ['appium:clock.currentTime']});
collectedEvents = [];
await B.delay(750);
collectedEvents.should.be.empty;
});
it('should allow custom bidi commands', async function () {
let {result} = await driver.send({method: 'appium:fake.getFakeThing', params: {}});
should.not.exist(result);
await driver.send({method: 'appium:fake.setFakeThing', params: {thing: 'this is from bidi'}});
({result} = await driver.send({method: 'appium:fake.getFakeThing', params: {}}));
result.should.eql('this is from bidi');
});
});
});
@@ -633,15 +677,14 @@ describe('Bidi over SSL', function () {
// since we update the FakeDriver.prototype below, make sure we update the FakeDriver which is
// actually going to be required by Appium
/** @type {import('@appium/types').DriverClass} */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let FakeDriver;
/** @type {string} */
let testServerBaseSessionUrl;
/** @type {import('sinon').SinonSandbox} */
let sandbox;
let certPath = 'certificate.cert';
let keyPath = 'certificate.key';
const capabilities = {...caps, webSocketUrl: true};
let should;
/** @type {string | undefined} */
let previousEnvValue;
before(async function () {
const chai = await import('chai');
@@ -657,7 +700,6 @@ describe('Bidi over SSL', function () {
}
return this.skip();
}
sandbox = createSandbox();
appiumHome = await tempDir.openDir();
wdOpts.port = port = await getTestPort();
testServerBaseUrl = `https://${TEST_HOST}:${port}`;
@@ -672,15 +714,20 @@ describe('Bidi over SSL', function () {
});
after(async function () {
await fs.rimraf(appiumHome);
await server.close();
if (server) {
await fs.rimraf(appiumHome);
await server.close();
}
});
beforeEach(async function () {
previousEnvValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
driver = await wdio({...wdOpts, protocol: 'https', strictSSL: false, capabilities});
});
afterEach(async function () {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = previousEnvValue;
if (driver) {
await driver.deleteSession();
}
+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);
});
});
});
});
/**
+1
View File
@@ -8,6 +8,7 @@ export default {
denyInsecure: [],
driversImportChunkSize: 3,
keepAliveTimeout: 600,
requestTimeout: 3600,
localTimezone: false,
logFormat: 'text',
loglevel: 'debug',
@@ -6,11 +6,12 @@ import {BaseDriver} from '@appium/base-driver';
import {FakeDriver} from '@appium/fake-driver';
import {sleep} from 'asyncbox';
import _ from 'lodash';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import {finalizeSchema, registerSchema, resetSchema} from '../../lib/schema/schema';
import {insertAppiumPrefixes, removeAppiumPrefixes} from '../../lib/utils';
import {rewiremock, BASE_CAPS, W3C_CAPS, W3C_PREFIXED_CAPS} from '../helpers';
import BasePlugin from '@appium/base-plugin';
import {BasePlugin} from '@appium/base-plugin';
const SESSION_ID = '1';
@@ -358,7 +359,7 @@ describe('AppiumDriver', function () {
_.keys(appium.sessions).should.not.contain(sessionId);
});
it('should remove session if inner driver unexpectedly exits with no error', async function () {
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value; // eslint-disable-line comma-spacing
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value;
_.keys(appium.sessions).should.contain(sessionId);
appium.sessions[sessionId].eventEmitter.emit('onUnexpectedShutdown');
// let event loop spin so rejection is handled
@@ -2,6 +2,7 @@ import {tempDir, fs, npm} from '@appium/support';
import {loadExtensions} from '../../../lib/extension';
import {Manifest} from '../../../lib/extension/manifest';
import DriverCommand from '../../../lib/cli/driver-command';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
describe('DriverCommand', function () {
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import {finalizeSchema, resetSchema, SchemaFinalizationError} from '../../../lib/schema/schema';
import {rewiremock} from '../../helpers';
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import {system} from '@appium/support';
import {getPresetDrivers} from '../../../lib/cli/setup-command';
@@ -1,6 +1,7 @@
// @ts-check
import fs from 'fs';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import YAML from 'yaml';
import * as schema from '../../lib/schema/schema';
@@ -212,14 +213,11 @@ describe('config-file', function () {
});
describe('when the config file is invalid', function () {
beforeEach(function () {
beforeEach(async function () {
lc.search.resolves({
config: {foo: 'bar'},
filepath: '/path/to/file.json',
});
});
beforeEach(async function () {
result = await readConfigFile();
});
+1
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';
+61
View File
@@ -3,6 +3,67 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.16.0](https://github.com/appium/appium/compare/@appium/base-driver@9.15.0...@appium/base-driver@9.16.0) (2025-02-19)
### Features
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
* **base-driver:** Print the closest match if the given script did not match ([#20956](https://github.com/appium/appium/issues/20956)) ([f8b5799](https://github.com/appium/appium/commit/f8b57999e5a4a13a089cff12954de5d99c8a35fc))
### Bug Fixes
* **driver-test-support:** update definitelytyped ([#20942](https://github.com/appium/appium/issues/20942)) ([f7c63ee](https://github.com/appium/appium/commit/f7c63ee5d87be21eba577a4cfd0b0a08a050afeb))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [9.15.0](https://github.com/appium/appium/compare/@appium/base-driver@9.14.1...@appium/base-driver@9.15.0) (2025-01-08)
### Features
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
## [9.14.1](https://github.com/appium/appium/compare/@appium/base-driver@9.14.0...@appium/base-driver@9.14.1) (2025-01-06)
### Bug Fixes
* **driver-test-support:** update dependency @types/lodash to v4.17.14 ([#20877](https://github.com/appium/appium/issues/20877)) ([fe209ed](https://github.com/appium/appium/commit/fe209ed660e0f790c98cd51938d0c75712cfd8e4))
## [9.14.0](https://github.com/appium/appium/compare/@appium/base-driver@9.13.1...@appium/base-driver@9.14.0) (2025-01-02)
### Features
* **appium:** Add session.status BiDi command ([#20839](https://github.com/appium/appium/issues/20839)) ([64e768e](https://github.com/appium/appium/commit/64e768efb7bebd6b5a24d55206d1cad00812777c))
* **appium:** allow drivers to define their own bidi commands ([#20828](https://github.com/appium/appium/issues/20828)) ([a917ec6](https://github.com/appium/appium/commit/a917ec6ceda2166fb3dcbff6b2768f700db9e103))
### Bug Fixes
* **base-driver:** Return an empty object if the corresponding API response is undefined ([#20845](https://github.com/appium/appium/issues/20845)) ([c765bc3](https://github.com/appium/appium/commit/c765bc31e03d37ed194fcd52f4e4a2ca1128b260))
* **base-driver:** update dependency express to v4.21.2 ([#20823](https://github.com/appium/appium/issues/20823)) ([c032352](https://github.com/appium/appium/commit/c032352fade2146cdce5feb0906726ad07a30e4f))
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
## [9.13.1](https://github.com/appium/appium/compare/@appium/base-driver@9.13.0...@appium/base-driver@9.13.1) (2024-12-05)
@@ -1,6 +1,7 @@
import type {Constraints, Driver, IBidiCommands} from '@appium/types';
import type {Constraints, DriverStatus, IBidiCommands} from '@appium/types';
import type {BaseDriver} from '../driver';
import {mixin} from './mixin';
import _ from 'lodash';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -32,6 +33,19 @@ const BidiCommands: IBidiCommands = {
}
}
},
async bidiStatus<C extends Constraints>(this: BaseDriver<C>): Promise<DriverStatus> {
const result = await this.getStatus();
if (!_.has(result, 'ready')) {
//@ts-ignore This is OK
result.ready = true;
}
if (!_.has(result, 'message')) {
//@ts-ignore This is OK
result.message = `${this.constructor.name} is ready to accept commands`;
}
return result as DriverStatus;
}
};
mixin(BidiCommands);
@@ -10,6 +10,7 @@ import {
} from '@appium/types';
import {mixin} from './mixin';
import {BaseDriver} from '../driver';
import {distance} from 'fastest-levenshtein';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -26,9 +27,33 @@ const ExecuteCommands: IExecuteCommands = {
const commandMetadata = {...Driver.executeMethodMap?.[script]};
if (!commandMetadata.command) {
const availableScripts = _.keys(Driver.executeMethodMap);
if (_.isEmpty(availableScripts)) {
throw new errors.UnsupportedOperationError(
`Unsupported execute method '${script}'. ` +
`Make sure the installed ${Driver.name} is up-to-date. ` +
`The current driver version does not define any execute methods.`
);
}
const matchesMap: StringRecord<string[]> = availableScripts
.map((name) => [distance(script, name), name])
.reduce((acc, [key, value]) => {
if (key in acc) {
acc[key].push(value);
} else {
acc[key] = [value];
}
return acc;
}, {});
const sortedMatches = _.flatten(
_.keys(matchesMap)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.map((x) => matchesMap[x])
);
throw new errors.UnsupportedOperationError(
`Unsupported execute method '${script}'. Available methods ` +
`are: ${availableScripts.join(', ')}`
`Unsupported execute method '${script}', did you mean '${sortedMatches[0]}'? ` +
`Make sure the installed ${Driver.name} is up-to-date. ` +
`Execute methods available in the current driver version are: ` +
sortedMatches.join(', ')
);
}
const args = validateExecuteMethodParams(protoArgs, commandMetadata.params);
@@ -5,7 +5,7 @@ import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends IFindCommands {}
}
@@ -10,6 +10,5 @@ import {BaseDriver} from '../driver';
* @param mixin Mixin implementation
*/
export function mixin<C extends Constraints, T extends Partial<BaseDriver<C>>>(mixin: T): void {
// eslint-disable-next-line no-restricted-syntax
Object.assign(BaseDriver.prototype, mixin);
}
@@ -76,19 +76,23 @@ const TimeoutCommands: ITimeoutCommands = {
},
// pageLoad
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async pageLoadTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async pageLoadTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
},
// script
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async scriptTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for script.');
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async scriptTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for script.');
},
@@ -99,7 +103,7 @@ const TimeoutCommands: ITimeoutCommands = {
},
setImplicitWait<C extends Constraints>(this: BaseDriver<C>, ms: number) {
// eslint-disable-line require-await
this.implicitWaitMs = ms;
this.log.debug(`Set implicit wait to ${ms}ms`);
if (this.managedDrivers && this.managedDrivers.length) {
+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;
}
}
+11 -1
View File
@@ -16,6 +16,7 @@ import {
type InitialOpts,
type DefaultDeleteSessionResult,
type SingularSessionData,
type SessionCapabilities,
} from '@appium/types';
import B from 'bluebird';
import _ from 'lodash';
@@ -24,7 +25,7 @@ import {calcSignature} from '../helpers/session';
import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
import {processCapabilities, validateCaps} from './capabilities';
import {DriverCore} from './core';
import helpers from './helpers';
import * as helpers from './helpers';
const EVENT_SESSION_INIT = 'newSessionRequested';
const EVENT_SESSION_START = 'newSessionStarted';
@@ -359,6 +360,8 @@ export class BaseDriver<
/**
* Returns capabilities for the session and event history (if applicable)
* @deprecated Use {@linkcode ISessionCommands.getAppiumSessionCapabilities} instead for getting the capabilities.
* Use {@linkcode EventCommands.getLogEvents} instead to get the event history.
*/
async getSession() {
return (
@@ -366,6 +369,13 @@ export class BaseDriver<
) as SingularSessionData<C, SessionData>;
}
/**
* Returns capabilities for the session
*/
async getAppiumSessionCapabilities(): Promise<SessionCapabilities<C>> {
return {capabilities: this.caps};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async deleteSession(sessionId?: string | null) {
await this.clearNewCommandTimeout();
@@ -0,0 +1,128 @@
import {logger} from '@appium/support';
import {EventEmitter} from 'node:events';
import type {
AppiumLogger,
BidiModuleMap,
BiDiResultData,
StringRecord,
} from '@appium/types';
import {
MAX_LOG_BODY_LENGTH,
} from '../constants';
import {errors} from '../protocol';
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
import _ from 'lodash';
import {generateDriverLogPrefix} from './helpers';
export class ExtensionCore {
bidiEventSubs: Record<string, string[]>;
bidiCommands: BidiModuleMap = BIDI_COMMANDS as BidiModuleMap;
_logPrefix?: string;
protected _log: AppiumLogger;
// used to handle driver events
readonly eventEmitter: NodeJS.EventEmitter;
constructor(logPrefix?: string) {
this._logPrefix = logPrefix;
this.bidiEventSubs = {};
this.eventEmitter = new EventEmitter();
}
get log(): AppiumLogger {
if (!this._log) {
this.updateLogPrefix(this._logPrefix ?? generateDriverLogPrefix(this));
}
return this._log;
}
updateLogPrefix(logPrefix: string) {
this._log = logger.getLogger(logPrefix);
}
updateBidiCommands(cmds: BidiModuleMap): void {
const overlappingKeys = _.intersection(Object.keys(cmds), Object.keys(this.bidiCommands));
if (overlappingKeys.length) {
this.log.warn(`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`);
}
this.bidiCommands = {
...this.bidiCommands,
...cmds,
};
}
doesBidiCommandExist(moduleName: string, methodName: string): boolean {
try {
this.ensureBidiCommandExists(moduleName, methodName);
} catch {
return false;
}
return true;
}
ensureBidiCommandExists(moduleName: string, methodName: string): void {
// if we don't get a valid format for bidi command name, reject
if (!moduleName || !methodName) {
throw new errors.UnknownCommandError(
`Did not receive a valid BiDi module and method name ` +
`of the form moduleName.methodName. Instead received ` +
`'${moduleName}.${methodName}'`,
);
}
// if the command module or method isn't part of our spec, reject
if (!this.bidiCommands[moduleName] || !this.bidiCommands[moduleName][methodName]) {
throw new errors.UnknownCommandError();
}
const {command} = this.bidiCommands[moduleName][methodName];
// if the command method isn't part of our spec, also reject
if (!command) {
throw new errors.UnknownCommandError();
}
// If the driver doesn't have this command, it must not be implemented
if (!this[command]) {
throw new errors.NotYetImplementedError();
}
}
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord, next?: () => Promise<any>, driver?: ExtensionCore): Promise<BiDiResultData> {
const handlerType = (next && driver) ? 'plugin' : 'driver';
const [moduleName, methodName] = bidiCmd.split('.');
this.ensureBidiCommandExists(moduleName, methodName);
const {command, params} = this.bidiCommands[moduleName][methodName];
// TODO improve param parsing and error messages along the lines of what we have in the http
// handlers
const args: any[] = [];
if (params?.required?.length) {
for (const requiredParam of params.required) {
if (_.isUndefined(bidiParams[requiredParam])) {
throw new errors.InvalidArgumentError(
`The ${requiredParam} parameter was required but you omitted it`,
);
}
args.push(bidiParams[requiredParam]);
}
}
if (params?.optional?.length) {
for (const optionalParam of params.optional) {
args.push(bidiParams[optionalParam]);
}
}
const logParams = _.truncate(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to ${handlerType} ` +
`method '${command}'`,
);
// call the handler with the signature appropriate to extension type (plugin or driver)
const response = (next && driver) ? await this[command](next, driver, ...args) : await this[command](...args);
const finalResponse = _.isUndefined(response) ? {} : response;
this.log.debug(
`Responding to bidi command '${bidiCmd}' with ` +
`${_.truncate(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`
);
return finalResponse;
}
}
@@ -389,8 +389,8 @@ export function parseCapsArray(cap) {
if (_.isArray(parsedCaps)) {
return parsedCaps;
}
} catch (ign) {
logger.warn(`Failed to parse capability as JSON array`);
} catch (e) {
logger.warn(`Failed to parse capability as JSON array: ${e.message}`);
}
if (_.isString(cap)) {
return [cap];
@@ -401,11 +401,12 @@ export function parseCapsArray(cap) {
/**
* Generate a string that uniquely describes driver instance
*
* @param {import('@appium/types').Core} obj driver instance
* @param {object} obj driver instance
* @param {string?} [sessionId=null] session identifier (if exists).
* This parameter is deprecated and is not used.
* @returns {string}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function generateDriverLogPrefix(obj, sessionId = null) {
return `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
}
@@ -49,7 +49,7 @@ const startLogFormatter = morgan(
reqBody = _.truncate(_.isString(req.body) ? req.body : JSON.stringify(req.body), {
length: MAX_LOG_BODY_LENGTH,
});
} catch (ign) {}
} catch {}
}
log.info(requestStartLoggingFormat(tokens, req, res), reqBody.grey);
},
+26 -7
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,
@@ -76,7 +76,7 @@ async function createServer (app, cliArgs) {
* @param {ServerOpts} opts
* @returns {Promise<AppiumServer>}
*/
async function server(opts) {
export async function server(opts) {
const {
routeConfiguringFunction,
port,
@@ -87,6 +87,7 @@ async function server(opts) {
extraMethodMap = {},
serverUpdaters = [],
keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS,
requestTimeout,
} = opts;
const app = express();
@@ -123,7 +124,13 @@ async function server(opts) {
// want to block extensions' ability to add routes if they want.
app.all('*', catch404Handler);
await startServer({httpServer, hostname, port, keepAliveTimeout});
await startServer({
httpServer,
hostname,
port,
keepAliveTimeout,
requestTimeout,
});
resolve(appiumServer);
} catch (err) {
@@ -136,7 +143,7 @@ async function server(opts) {
* Sets up some Express middleware and stuff
* @param {ConfigureServerOpts} opts
*/
function configureServer({
export function configureServer({
app,
addRoutes,
allowCors = true,
@@ -151,6 +158,7 @@ function configureServer({
// set up static assets
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
// eslint-disable-next-line import/no-named-as-default-member
app.use(express.static(STATIC_DIR));
// crash routes, for testing
@@ -266,13 +274,22 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
* @param {StartServerOpts} opts
* @returns {Promise<void>}
*/
async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
async function startServer({
httpServer,
port,
hostname,
keepAliveTimeout,
requestTimeout,
}) {
// If the hostname is omitted, the server will accept
// connections on any IP address
/** @type {(port: number, hostname?: string) => B<http.Server>} */
const start = B.promisify(httpServer.listen, {context: httpServer});
const startPromise = start(port, hostname);
httpServer.keepAliveTimeout = keepAliveTimeout;
if (_.isInteger(requestTimeout)) {
httpServer.requestTimeout = Number(requestTimeout);
}
// headers timeout must be greater than keepAliveTimeout
httpServer.headersTimeout = keepAliveTimeout + 5 * 1000;
await startPromise;
@@ -283,7 +300,7 @@ async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
* @param {string} basePath
* @returns {string}
*/
function normalizeBasePath(basePath) {
export function normalizeBasePath(basePath) {
if (!_.isString(basePath)) {
throw new Error(`Invalid path prefix ${basePath}`);
}
@@ -301,7 +318,6 @@ function normalizeBasePath(basePath) {
return basePath;
}
export {server, configureServer, normalizeBasePath};
/**
* Options for {@linkcode startServer}.
@@ -310,6 +326,8 @@ export {server, configureServer, normalizeBasePath};
* @property {number} port - Port to run on
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
* @property {string} [hostname] - Optional hostname
* @property {number} [requestTimeout] - The timeout value in milliseconds for
* receiving the entire request from the client
*/
/**
@@ -343,6 +361,7 @@ export {server, configureServer, normalizeBasePath};
* @property {MethodMap} [extraMethodMap]
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
* @property {number} [keepAliveTimeout]
* @property {number} [requestTimeout]
*/
/**
@@ -1,4 +1,4 @@
/* eslint-disable require-await */
import _ from 'lodash';
import B from 'bluebird';
@@ -41,7 +41,7 @@ async function removeWebSocketHandler(handlerPathname) {
client.terminate();
}
return true;
} catch (ign) {
} catch {
// ignore
} finally {
delete this.webSocketsMapping[handlerPathname];
+7 -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,11 @@ export {
// Web socket helpers
export {DEFAULT_WS_PATHNAME_PREFIX} from './express/websocket';
// BiDi exports
export {BIDI_COMMANDS} from './protocol/bidi-commands';
export {generateDriverLogPrefix} from './basedriver/helpers';
/**
* @typedef {import('./express/server').ServerOpts} ServerOpts
*/
+21 -4
View File
@@ -15,6 +15,7 @@ import ProtocolConverter from './protocol-converter';
import {formatResponseValue, formatStatus} from '../protocol/helpers';
import http from 'http';
import https from 'https';
import { match as pathToRegexMatch } from 'path-to-regexp';
const DEFAULT_LOG = logger.getLogger('WD Proxy');
const DEFAULT_REQUEST_TIMEOUT = 240000;
@@ -110,8 +111,24 @@ class JWProxy {
this._activeRequests = [];
}
/**
* Return true if the given endpoint started with '/session' and
* it could have session id after the path.
* e.g.
* - should return true
* - /session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200
* - /session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/url
* - should return false
* - /session
* - /sessions
* - /session/
* - /status
* - /appium/sessions
* @param {string} endpoint
* @returns {boolean}
*/
endpointRequiresSessionId(endpoint) {
return !_.includes(['/session', '/sessions', '/status'], endpoint);
return !!(pathToRegexMatch('/session/:sessionId{/*command}')(endpoint));
}
set downstreamProtocol(value) {
@@ -128,7 +145,7 @@ class JWProxy {
url = '/';
}
const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
const endpointRe = '(/(session|status))';
const endpointRe = '(/(session|status|appium))';
let remainingUrl = '';
if (/^http/.test(url)) {
const first = new RegExp(`(https?://.+)${endpointRe}`).exec(url);
@@ -142,7 +159,7 @@ class JWProxy {
throw new Error(`Did not know what to do with url '${url}'`);
}
const stripPrefixRe = new RegExp('^.*?(/(session|status).*)$');
const stripPrefixRe = new RegExp('^.*?(/(session|status|appium).*)$');
if (stripPrefixRe.test(remainingUrl)) {
remainingUrl = /** @type {RegExpExecArray} */ (stripPrefixRe.exec(remainingUrl))[1];
}
@@ -207,7 +224,7 @@ class JWProxy {
if (typeof body !== 'object') {
try {
reqOpts.data = JSON.parse(body);
} catch (e) {
} catch {
throw new Error(`Cannot interpret the request body as valid JSON: ${truncateBody(body)}`);
}
} else {
@@ -3,7 +3,7 @@ const SUBSCRIPTION_REQUEST_PARAMS = /** @type {const} */ ({
optional: ['contexts'],
});
const BIDI_COMMANDS = /** @type {const} */ ({
export const BIDI_COMMANDS = /** @type {const} */ ({
session: {
subscribe: {
command: 'bidiSubscribe',
@@ -13,6 +13,10 @@ const BIDI_COMMANDS = /** @type {const} */ ({
command: 'bidiUnsubscribe',
params: SUBSCRIPTION_REQUEST_PARAMS,
},
status: {
command: 'bidiStatus',
params: {},
}
},
browsingContext: {
navigate: {
@@ -27,5 +31,3 @@ const BIDI_COMMANDS = /** @type {const} */ ({
// TODO add definitions for all bidi commands.
// spec link: https://w3c.github.io/webdriver-bidi/
export {BIDI_COMMANDS};
+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,
@@ -5,6 +5,8 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
makeArgs,
checkParams,
validateExecuteMethodParams,
@@ -30,4 +32,6 @@ export {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
};
@@ -16,9 +16,11 @@ import {isW3cCaps} from '../helpers/capabilities';
import log from '../basedriver/logger';
import { generateDriverLogPrefix } from '../basedriver/helpers';
const CREATE_SESSION_COMMAND = 'createSession';
const DELETE_SESSION_COMMAND = 'deleteSession';
const GET_STATUS_COMMAND = 'getStatus';
export const CREATE_SESSION_COMMAND = 'createSession';
export const DELETE_SESSION_COMMAND = 'deleteSession';
export const GET_STATUS_COMMAND = 'getStatus';
export const LIST_DRIVER_COMMANDS_COMMAND = 'listCommands';
export const LIST_DRIVER_EXTENSIONS_COMMAND = 'listExtensions';
/** @type {Set<string>} */
const deprecatedCommandsLogged = new Set();
@@ -559,9 +561,6 @@ export {
isSessionCommand,
driverShouldDoJwpProxy,
determineProtocol,
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
deprecatedCommandsLogged,
validateExecuteMethodParams,
};
+379 -364
View File
@@ -22,6 +22,12 @@ const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
* @satisfies {import('@appium/types').MethodMap<import('../basedriver/driver').BaseDriver>}
*/
export const METHOD_MAP = /** @type {const} */ ({
//
// W3C WebDriver (and deprecated MJSONWP that will be removed in Appium 3)
// https://www.w3.org/TR/webdriver1/
// https://www.w3.org/TR/webdriver2/
//
'/status': {
GET: {command: 'getStatus'},
},
@@ -29,6 +35,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {
command: 'createSession',
payloadParams: {
// TODO: Appium 3 will accept only 'capabilities'.
validate: (jsonObj) =>
!jsonObj.capabilities &&
!jsonObj.desiredCapabilities &&
@@ -37,11 +44,8 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/sessions': {
GET: {command: 'getSessions'},
},
'/session/:sessionId': {
GET: {command: 'getSession'},
GET: {command: 'getSession', deprecated: true},
DELETE: {command: 'deleteSession'},
},
'/session/:sessionId/timeouts': {
@@ -59,7 +63,7 @@ export const METHOD_MAP = /** @type {const} */ ({
return 'W3C protocol expects any of script, pageLoad or implicit to be set';
}
} else {
// MJSONWP
// TODO: Remove in Appium 3
if (!util.hasValue(jsonObj.type) || !util.hasValue(jsonObj.ms)) {
return 'MJSONWP protocol requires type and ms';
}
@@ -69,25 +73,6 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/session/:sessionId/timeouts/async_script': {
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/timeouts/implicit_wait': {
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
},
// JSONWP
'/session/:sessionId/window_handle': {
GET: {command: 'getWindowHandle'},
},
// W3C
'/session/:sessionId/window/handle': {
GET: {command: 'getWindowHandle'},
},
// JSONWP
'/session/:sessionId/window_handles': {
GET: {command: 'getWindowHandles'},
},
// W3C
'/session/:sessionId/window/handles': {
GET: {command: 'getWindowHandles'},
},
@@ -104,39 +89,10 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/refresh': {
POST: {command: 'refresh'},
},
// MJSONWP
'/session/:sessionId/execute': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
// MJSONWP
'/session/:sessionId/execute_async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
'/session/:sessionId/screenshot': {
GET: {command: 'getScreenshot'},
},
'/session/:sessionId/ime/available_engines': {
GET: {command: 'availableIMEEngines', deprecated: true},
},
'/session/:sessionId/ime/active_engine': {
GET: {command: 'getActiveIMEEngine', deprecated: true},
},
'/session/:sessionId/ime/activated': {
GET: {command: 'isIMEActivated', deprecated: true},
},
'/session/:sessionId/ime/deactivate': {
POST: {command: 'deactivateIMEEngine', deprecated: true},
},
'/session/:sessionId/ime/activate': {
POST: {
command: 'activateIMEEngine',
payloadParams: {required: ['engine']},
deprecated: true,
},
},
'/session/:sessionId/frame': {
POST: {command: 'setFrame', payloadParams: {required: ['id']}},
},
@@ -148,6 +104,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {
command: 'setWindow',
payloadParams: {
// TODO: Appium 3 will only accept 'handle'. 'name' will be ginored.
optional: ['name', 'handle'],
// Return both values to match W3C and JSONWP protocols
makeArgs: (jsonObj) => {
@@ -167,16 +124,18 @@ export const METHOD_MAP = /** @type {const} */ ({
},
DELETE: {command: 'closeWindow'},
},
'/session/:sessionId/window/:windowhandle/size': {
GET: {command: 'getWindowSize', deprecated: true},
},
'/session/:sessionId/window/:windowhandle/position': {
POST: {deprecated: true},
GET: {deprecated: true},
},
'/session/:sessionId/window/:windowhandle/maximize': {
'/session/:sessionId/window/maximize': {
POST: {command: 'maximizeWindow'},
},
'/session/:sessionId/window/minimize': {
POST: {command: 'minimizeWindow'},
},
'/session/:sessionId/window/fullscreen': {
POST: {command: 'fullScreenWindow'},
},
'/session/:sessionId/window/new': {
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/cookie': {
GET: {command: 'getCookies'},
POST: {command: 'setCookie', payloadParams: {required: ['cookie']}},
@@ -206,7 +165,7 @@ export const METHOD_MAP = /** @type {const} */ ({
},
'/session/:sessionId/element/active': {
GET: {command: 'active'}, // W3C: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-get-active-element
POST: {command: 'active'},
POST: {command: 'active', deprecated: true},
},
'/session/:sessionId/element/:elementId': {
GET: {},
@@ -226,9 +185,6 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/click': {
POST: {command: 'click'},
},
'/session/:sessionId/element/:elementId/submit': {
POST: {command: 'submit', deprecated: true},
},
'/session/:sessionId/element/:elementId/text': {
GET: {command: 'getText'},
},
@@ -240,6 +196,7 @@ export const METHOD_MAP = /** @type {const} */ ({
!util.hasValue(jsonObj.value) &&
!util.hasValue(jsonObj.text) &&
'we require one of "text" or "value" params',
// TODO: Appium 3 will accept only 'value'.
optional: ['value', 'text'],
// override the default argument constructor because of the special
// logic here. Basically we want to accept either a value (old JSONWP)
@@ -250,9 +207,6 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/session/:sessionId/keys': {
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
},
'/session/:sessionId/element/:elementId/name': {
GET: {command: 'getName'},
},
@@ -268,21 +222,9 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/attribute/:name': {
GET: {command: 'getAttribute'},
},
'/session/:sessionId/element/:elementId/equals/:otherId': {
GET: {command: 'equalsElement', deprecated: true},
},
'/session/:sessionId/element/:elementId/displayed': {
GET: {command: 'elementDisplayed'},
},
'/session/:sessionId/element/:elementId/location': {
GET: {command: 'getLocation', deprecated: true},
},
'/session/:sessionId/element/:elementId/location_in_view': {
GET: {command: 'getLocationInView', deprecated: true},
},
'/session/:sessionId/element/:elementId/size': {
GET: {command: 'getSize', deprecated: true},
},
'/session/:sessionId/element/:elementId/shadow': {
GET: {command: 'elementShadowRoot'},
},
@@ -301,12 +243,8 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/css/:propertyName': {
GET: {command: 'getCssProperty'},
},
'/session/:sessionId/orientation': {
GET: {command: 'getOrientation'},
POST: {
command: 'setOrientation',
payloadParams: {required: ['orientation']},
},
'/session/:sessionId/element/:elementId/property/:name': {
GET: {command: 'getProperty'},
},
// w3c v2 https://www.w3.org/TR/webdriver2/#get-computed-role
'session/:sessionId/element/:elementId/computedrole': {
@@ -316,10 +254,336 @@ export const METHOD_MAP = /** @type {const} */ ({
'session/:sessionId/element/:elementId/computedlabel': {
GET: {command: 'getComputedLabel'},
},
'/session/:sessionId/actions': {
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
DELETE: {command: 'releaseActions'},
},
'/session/:sessionId/alert/text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
'/session/:sessionId/alert/accept': {
POST: {command: 'postAcceptAlert'},
},
'/session/:sessionId/alert/dismiss': {
POST: {command: 'postDismissAlert'},
},
'/session/:sessionId/element/:elementId/rect': {
GET: {command: 'getElementRect'},
},
'/session/:sessionId/execute/sync': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
'/session/:sessionId/execute/async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
'/session/:sessionId/element/:elementId/screenshot': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/window/rect': {
GET: {command: 'getWindowRect'},
POST: {
command: 'setWindowRect',
payloadParams: {optional: ['x', 'y', 'width', 'height']},
},
},
//
// Appium specific
//
'/session/:sessionId/ime/available_engines': {
GET: {command: 'availableIMEEngines'},
},
'/session/:sessionId/ime/active_engine': {
GET: {command: 'getActiveIMEEngine'},
},
'/session/:sessionId/ime/activated': {
GET: {command: 'isIMEActivated'},
},
'/session/:sessionId/ime/deactivate': {
POST: {command: 'deactivateIMEEngine'},
},
'/session/:sessionId/ime/activate': {
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}},
},
'/session/:sessionId/rotation': {
GET: {command: 'getRotation'},
POST: {command: 'setRotation', payloadParams: {required: ['x', 'y', 'z']}},
},
'/session/:sessionId/location': {
GET: {command: 'getGeoLocation'},
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
},
'/session/:sessionId/orientation': {
GET: {command: 'getOrientation'},
POST: {
command: 'setOrientation',
payloadParams: {required: ['orientation']}
},
},
'/session/:sessionId/context': {
GET: {command: 'getCurrentContext'},
POST: {command: 'setContext', payloadParams: {required: ['name']}},
},
'/session/:sessionId/contexts': {
GET: {command: 'getContexts'},
},
'/session/:sessionId/network_connection': {
GET: {command: 'getNetworkConnection'},
POST: {
command: 'setNetworkConnection',
payloadParams: {unwrap: 'parameters', required: ['type']},
},
},
'/session/:sessionId/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
payloadParams: {required: ['status', 'value']},
},
},
'/appium/sessions': {
GET: {command: 'getAppiumSessions'},
},
'/session/:sessionId/appium/capabilities': {
GET: {command: 'getAppiumSessionCapabilities'}
},
'/session/:sessionId/appium/device/system_time': {
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
},
// #region Applications Management
'/session/:sessionId/appium/device/install_app': {
POST: {
command: 'installApp',
payloadParams: {
required: ['appPath'],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/activate_app': {
POST: {
command: 'activateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/remove_app': {
POST: {
command: 'removeApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/terminate_app': {
POST: {
command: 'terminateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/app_installed': {
POST: {
command: 'isAppInstalled',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
},
'/session/:sessionId/appium/device/app_state': {
GET: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
POST: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
deprecated: true,
},
},
// #endregion
'/session/:sessionId/appium/device/hide_keyboard': {
POST: {
command: 'hideKeyboard',
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
},
},
'/session/:sessionId/appium/device/is_keyboard_shown': {
GET: {command: 'isKeyboardShown'},
},
'/session/:sessionId/appium/device/push_file': {
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
},
'/session/:sessionId/appium/device/pull_file': {
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/pull_folder': {
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/settings': {
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
GET: {command: 'getSettings'},
},
'/session/:sessionId/appium/events': {
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/appium/log_event': {
POST: {
command: 'logCustomEvent',
payloadParams: {required: ['vendor', 'event']},
},
},
// #region Inspector
'/session/:sessionId/appium/commands': {
GET: {command: 'listCommands'},
},
'/session/:sessionId/appium/extensions': {
GET: {command: 'listExtensions'},
},
// #endregion
//
// 3rd party vendor/protcol support
//
// #region Selenium/Chromium browsers
'/session/:sessionId/se/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
'/session/:sessionId/se/log/types': {
GET: {command: 'getLogTypes'},
},
// #endregion
// #region chromium devtools
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
'/session/:sessionId/:vendor/cdp/execute': {
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
},
// #endregion
// #region Webauthn
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
'/session/:sessionId/webauthn/authenticator': {
POST: {
command: 'addVirtualAuthenticator',
payloadParams: {
required: ['protocol', 'transport'],
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
DELETE: {
command: 'removeVirtualAuthenticator',
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
POST: {
command: 'addAuthCredential',
payloadParams: {
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
optional: ['userHandle', 'signCount'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
GET: {command: 'getAuthCredential'},
DELETE: {command: 'removeAllAuthCredentials'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
DELETE: {command: 'removeAuthCredential'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
POST: {
command: 'setUserAuthVerified',
payloadParams: {
required: ['isUserVerified'],
},
},
},
// #endregion
//
// Endpoints deprecated entirely
//
// #region MJSONWP
'/sessions': {
GET: {command: 'getSessions', deprecated: true},
},
'/session/:sessionId/timeouts/async_script': {
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/timeouts/implicit_wait': {
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/window_handle': {
GET: {command: 'getWindowHandle', deprecated: true},
},
// Only 'window/handles' exists in W3C WebDriver spec.
'/session/:sessionId/window/handle': {
GET: {command: 'getWindowHandle', deprecated: true},
},
'/session/:sessionId/window_handles': {
GET: {command: 'getWindowHandles', deprecated: true},
},
'/session/:sessionId/execute': {
POST: {
command: 'execute',
payloadParams: {required: ['script', 'args']},
deprecated: true
},
},
'/session/:sessionId/execute_async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
deprecated: true
},
},
'/session/:sessionId/window/:windowhandle/size': {
GET: {command: 'getWindowSize', deprecated: true},
},
'/session/:sessionId/window/:windowhandle/position': {
POST: {deprecated: true},
GET: {deprecated: true},
},
'/session/:sessionId/window/:windowhandle/maximize': {
POST: {command: 'maximizeWindow', deprecated: true},
},
'/session/:sessionId/element/:elementId/submit': {
POST: {command: 'submit', deprecated: true},
},
'/session/:sessionId/keys': {
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
},
'/session/:sessionId/element/:elementId/equals/:otherId': {
GET: {command: 'equalsElement', deprecated: true},
},
'/session/:sessionId/element/:elementId/location': {
GET: {command: 'getLocation', deprecated: true},
},
'/session/:sessionId/element/:elementId/location_in_view': {
GET: {command: 'getLocationInView', deprecated: true},
},
'/session/:sessionId/element/:elementId/size': {
GET: {command: 'getSize', deprecated: true},
},
'/session/:sessionId/moveto': {
POST: {
command: 'moveTo',
@@ -355,11 +619,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {deprecated: true},
},
'/session/:sessionId/touch/doubleclick': {
POST: {},
},
'/session/:sessionId/actions': {
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
DELETE: {command: 'releaseActions'},
POST: {deprecated: true},
},
'/session/:sessionId/touch/longclick': {
POST: {
@@ -377,10 +637,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/location': {
GET: {command: 'getGeoLocation'},
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
},
'/session/:sessionId/local_storage': {
GET: {deprecated: true},
POST: {deprecated: true},
@@ -405,46 +661,32 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/session_storage/size': {
GET: {deprecated: true},
},
// Selenium 4 clients
'/session/:sessionId/se/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
// Selenium 4 clients
'/session/:sessionId/se/log/types': {
GET: {command: 'getLogTypes'},
},
// mjsonwire, appium clients
'/session/:sessionId/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
// mjsonwire, appium clients
'/session/:sessionId/log/types': {
GET: {command: 'getLogTypes'},
},
'/session/:sessionId/application_cache/status': {
GET: {},
GET: {deprecated: true},
},
//
// mjsonwire
//
'/session/:sessionId/context': {
GET: {command: 'getCurrentContext'},
POST: {command: 'setContext', payloadParams: {required: ['name']}},
'/session/:sessionId/alert_text': {
GET: {command: 'getAlertText', deprecated: true},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
deprecated: true
},
},
'/session/:sessionId/contexts': {
GET: {command: 'getContexts'},
'/session/:sessionId/accept_alert': {
POST: {command: 'postAcceptAlert', deprecated: true},
},
'/session/:sessionId/dismiss_alert': {
POST: {command: 'postDismissAlert', deprecated: true},
},
// Pre-W3C endpoint for element screenshot
'/session/:sessionId/screenshot/:elementId': {
GET: {command: 'getElementScreenshot', deprecated: true},
},
// #endregion
// #region Appium specific
'/session/:sessionId/element/:elementId/pageIndex': {
GET: {command: 'getPageIndex', deprecated: true},
},
'/session/:sessionId/network_connection': {
GET: {command: 'getNetworkConnection'},
POST: {
command: 'setNetworkConnection',
payloadParams: {unwrap: 'parameters', required: ['type']},
},
},
'/session/:sessionId/touch/perform': {
POST: {
command: 'performTouch',
@@ -459,19 +701,9 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
payloadParams: {required: ['status', 'value']},
},
},
'/session/:sessionId/appium/device/shake': {
POST: {command: 'mobileShake', deprecated: true},
},
'/session/:sessionId/appium/device/system_time': {
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
},
'/session/:sessionId/appium/device/lock': {
POST: {command: 'lock', payloadParams: {optional: ['seconds']}, deprecated: true},
},
@@ -575,85 +807,6 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/appium/device/current_package': {
GET: {command: 'getCurrentPackage', deprecated: true},
},
//region Applications Management
'/session/:sessionId/appium/device/install_app': {
POST: {
command: 'installApp',
payloadParams: {
required: ['appPath'],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/activate_app': {
POST: {
command: 'activateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/remove_app': {
POST: {
command: 'removeApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/terminate_app': {
POST: {
command: 'terminateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/app_installed': {
POST: {
command: 'isAppInstalled',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
},
'/session/:sessionId/appium/device/app_state': {
GET: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
POST: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
deprecated: true,
},
},
//endregion
'/session/:sessionId/appium/device/hide_keyboard': {
POST: {
command: 'hideKeyboard',
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
},
},
'/session/:sessionId/appium/device/is_keyboard_shown': {
GET: {command: 'isKeyboardShown'},
},
'/session/:sessionId/appium/device/push_file': {
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
},
'/session/:sessionId/appium/device/pull_file': {
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/pull_folder': {
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/toggle_airplane_mode': {
POST: {command: 'toggleFlightMode', deprecated: true},
},
@@ -743,10 +896,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/appium/settings': {
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
GET: {command: 'getSettings'},
},
'/session/:sessionId/appium/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
@@ -754,94 +903,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/appium/events': {
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/appium/log_event': {
POST: {
command: 'logCustomEvent',
payloadParams: {required: ['vendor', 'event']},
},
},
/*
* The W3C spec has some changes to the wire protocol.
* https://w3c.github.io/webdriver/webdriver-spec.html
* Begin to add those changes here, keeping the old version
* since clients still implement them.
*/
// MJSONWP
'/session/:sessionId/alert_text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
// MJSONWP
'/session/:sessionId/accept_alert': {
POST: {command: 'postAcceptAlert'},
},
// MJSONWP
'/session/:sessionId/dismiss_alert': {
POST: {command: 'postDismissAlert'},
},
// https://w3c.github.io/webdriver/webdriver-spec.html#user-prompts
'/session/:sessionId/alert/text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
'/session/:sessionId/alert/accept': {
POST: {command: 'postAcceptAlert'},
},
'/session/:sessionId/alert/dismiss': {
POST: {command: 'postDismissAlert'},
},
// https://w3c.github.io/webdriver/webdriver-spec.html#get-element-rect
'/session/:sessionId/element/:elementId/rect': {
GET: {command: 'getElementRect'},
},
'/session/:sessionId/execute/sync': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
'/session/:sessionId/execute/async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
// Pre-W3C endpoint for element screenshot
'/session/:sessionId/screenshot/:elementId': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/element/:elementId/screenshot': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/window/rect': {
GET: {command: 'getWindowRect'},
POST: {
command: 'setWindowRect',
payloadParams: {optional: ['x', 'y', 'width', 'height']},
},
},
'/session/:sessionId/window/maximize': {
POST: {command: 'maximizeWindow'},
},
'/session/:sessionId/window/minimize': {
POST: {command: 'minimizeWindow'},
},
'/session/:sessionId/window/fullscreen': {
POST: {command: 'fullScreenWindow'},
},
'/session/:sessionId/window/new': {
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/element/:elementId/property/:name': {
GET: {command: 'getProperty'},
},
'/session/:sessionId/appium/device/set_clipboard': {
POST: {
command: 'setClipboard',
@@ -861,61 +922,15 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
// chromium devtools
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
'/session/:sessionId/:vendor/cdp/execute': {
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
// #endregion
// #region JSONWP
'/session/:sessionId/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}, deprecated: true},
},
//region Webauthn
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
'/session/:sessionId/webauthn/authenticator': {
POST: {
command: 'addVirtualAuthenticator',
payloadParams: {
required: ['protocol', 'transport'],
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
},
},
'/session/:sessionId/log/types': {
GET: {command: 'getLogTypes', deprecated: true},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
DELETE: {
command: 'removeVirtualAuthenticator',
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
POST: {
command: 'addAuthCredential',
payloadParams: {
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
optional: ['userHandle', 'signCount'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
GET: {command: 'getAuthCredential'},
DELETE: {command: 'removeAllAuthCredentials'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
DELETE: {command: 'removeAuthCredential'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
POST: {
command: 'setUserAuthVerified',
payloadParams: {
required: ['isUserVerified'],
},
},
},
//endregion
// #endregion
});
// driver command names
@@ -963,4 +978,4 @@ export function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PAT
}
// driver commands that do not require a session to already exist
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions'];
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions', 'getAppiumSessions'];
+6 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "9.13.1",
"version": "9.16.0",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -44,21 +44,16 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^6.0.0",
"@appium/types": "^0.22.3",
"@appium/support": "^6.0.4",
"@appium/types": "^0.25.0",
"@colors/colors": "1.6.0",
"@types/async-lock": "1.4.2",
"@types/bluebird": "3.5.42",
"@types/express": "5.0.0",
"@types/lodash": "4.17.13",
"@types/method-override": "3.0.0",
"@types/serve-favicon": "2.5.7",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.7.9",
"bluebird": "3.7.2",
"body-parser": "1.20.3",
"express": "4.21.1",
"express": "4.21.2",
"fastest-levenshtein": "1.0.16",
"http-status-codes": "2.3.0",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
@@ -67,7 +62,7 @@
"path-to-regexp": "8.2.0",
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"type-fest": "4.30.0",
"type-fest": "4.35.0",
"validate.js": "0.13.1"
},
"optionalDependencies": {
@@ -1,13 +1,63 @@
import {BaseDriver} from '../../../lib';
import {driverE2ETestSuite} from '@appium/driver-test-support';
import { BaseDriver, server, routeConfiguringFunction } from '../../../lib';
import { FakeDriver } from '../protocol/fake-driver';
import axios from 'axios';
import {TEST_HOST, getTestPort, driverE2ETestSuite} from '@appium/driver-test-support';
const DEFAULT_CAPS = {
platformName: 'iOS',
'appium:deviceName': 'Delorean',
platformName: 'fake',
'appium:automationNAme': 'fake',
};
driverE2ETestSuite(BaseDriver, DEFAULT_CAPS);
driverE2ETestSuite(BaseDriver, {
platformName: 'iOS',
'appium:deviceName': 'Delorean',
});
/**
* @typedef {import('@appium/driver-test-support').SessionHelpers} SessionHelpers
*/
describe('BaseDriver', function () {
let port;
let baseUrl;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
port = await getTestPort();
baseUrl = `http://${TEST_HOST}:${port}`;
});
describe('get appium capabilities', function () {
let driver;
let sessionId = 'foo';
let mjsonwpServer;
before(async function () {
driver = new FakeDriver();
driver.sessionId = sessionId;
mjsonwpServer = await server({
routeConfiguringFunction: routeConfiguringFunction(driver),
port,
});
});
after(async function () {
await mjsonwpServer.close();
});
it('should return capabilities', async function () {
const capabilities = DEFAULT_CAPS;
driver.caps = capabilities;
const {data} = await axios({
url: `${baseUrl}/session/${sessionId}/appium/capabilities`,
method: 'GET',
});
data.should.eql({
value: {
capabilities: DEFAULT_CAPS
},
sessionId
});
});
});
});
@@ -1,5 +1,6 @@
import {server} from '../../../lib';
import axios from 'axios';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import B from 'bluebird';
import _ from 'lodash';
@@ -85,7 +86,11 @@ describe('server', function () {
}).should.be.rejectedWith(/EADDRINUSE/);
});
it('should not wait for the server close connections before finishing closing', async function () {
let bodyPromise = axios.get(`http://${TEST_HOST}:${port}/pause`).catch(() => {});
const bodyPromise = (async () => {
try {
return await axios.get(`http://${TEST_HOST}:${port}/pause`);
} catch {}
})();
// relinquish control so that we don't close before the request is received
await B.delay(100);
@@ -188,11 +193,11 @@ describe('server plugins', function () {
afterEach(async function () {
try {
await hwServer.close();
} catch (ign) {}
} catch {}
});
function updaterWithGetRoute(route, reply) {
// eslint-disable-next-line require-await
return async (app, httpServer) => {
app.get(`/${route}`, (req, res) => {
res.header['content-type'] = 'text/html';
@@ -1,4 +1,4 @@
/* eslint-disable require-await */
import {errors, BaseDriver, determineProtocol} from '../../../lib';
import {PROTOCOLS} from '../../../lib/constants';
import {util} from '@appium/support';
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-named-as-default
import Express from 'express';
import bodyParser from 'body-parser';
@@ -1,6 +1,7 @@
import {server, routeConfiguringFunction, errors, JWProxy, BaseDriver} from '../../../lib';
import {FakeDriver} from './fake-driver';
import axios from 'axios';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import {StatusCodes as HTTPStatusCodes} from 'http-status-codes';
import {createProxyServer} from './helpers';
@@ -917,7 +918,7 @@ describe('Protocol', function () {
});
it('should pass on any errors in proxying', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function () {
throw new Error('foo');
};
@@ -938,7 +939,7 @@ describe('Protocol', function () {
});
it('should able to throw ProxyRequestError in proxying', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function () {
let jsonwp = {
status: 35,
@@ -961,7 +962,7 @@ describe('Protocol', function () {
});
it('should let the proxy handle req/res', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function (req, res) {
res.status(200).json({custom: 'data'});
};
@@ -3,6 +3,7 @@
import B from 'bluebird';
import {BaseDriver, errors} from '../../../lib/index';
import {validator} from '../../../lib/basedriver/desired-caps';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
// TODO: we need module-level mocks for the logger
@@ -1,7 +1,8 @@
// @ts-check
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import _ from 'lodash';
import BaseDriver from '../../../../lib';
import {BaseDriver} from '../../../../lib';
const FIRST_LOGS = ['first', 'logs'];
const SECOND_LOGS = ['second', 'logs'];
@@ -1,4 +1,4 @@
import BaseDriver from '../../../lib';
import {BaseDriver} from '../../../lib';
import {driverUnitTestSuite} from '@appium/driver-test-support';
driverUnitTestSuite(BaseDriver, {
@@ -1,6 +1,7 @@
// @ts-check
import BaseDriver from '../../../lib';
import {BaseDriver} from '../../../lib';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
describe('timeout', function () {
@@ -2,6 +2,7 @@
import {server, routeConfiguringFunction} from '../../../lib';
import {configureServer, normalizeBasePath} from '../../../lib/express/server';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
import {getTestPort} from '@appium/driver-test-support';
@@ -31,6 +32,8 @@ describe('server configuration', function () {
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
should = chai.should();
port = await getTestPort(true);
});
function fakeApp() {
@@ -50,10 +53,6 @@ describe('server configuration', function () {
return app;
}
before(async function () {
port = await getTestPort(true);
});
beforeEach(function () {
sandbox = createSandbox();
});
@@ -1,6 +1,5 @@
// transpile:mocha
import {welcome} from '../../../lib/express/static';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
describe('welcome', function () {
@@ -20,7 +20,7 @@ function resFixture(url, method) {
throw new Error("Can't handle url " + url);
}
// eslint-disable-next-line require-await
async function request(opts) {
const {url, method, json} = opts;
if (/badurl$/.test(url)) {
@@ -17,7 +17,7 @@ function buildReqRes(url, method, body) {
res.send = (body) => {
try {
body = JSON.parse(body);
} catch (e) {}
} catch {}
res.sentBody = body;
};
return [req, res];
@@ -32,6 +32,7 @@ describe('proxy', function () {
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
should = chai.should();
port = await getTestPort();
});
function mockProxy(opts = {}) {
@@ -44,10 +45,6 @@ describe('proxy', function () {
return proxy;
}
before(async function () {
port = await getTestPort();
});
it('should override default params', function () {
let j = mockProxy({server: '127.0.0.2', port});
j.server.should.equal('127.0.0.2');
@@ -110,6 +107,12 @@ describe('proxy', function () {
let j = mockProxy();
let newUrl = j.getUrlForProxy('/status');
should.exist(newUrl);
});
it('should not throw an error if url does not require a session id with appium vendor prefix and its null', function () {
let j = mockProxy();
let newUrl = j.getUrlForProxy('/appium/something');
should.exist(newUrl);
});
});
@@ -212,4 +215,30 @@ describe('proxy', function () {
res.sentBody.should.eql({value: {message: 'chrome not reachable'}});
});
});
describe('endpointRequiresSessionId', function () {
const j = mockProxy({sessionId: '123'});
[
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200',
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/',
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/url',
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/element/3d001db2-7987-42a7-975d-8d5d5304083f',
].forEach(function (endpoint) {
it(`should be true with ${endpoint}`, function () {
j.endpointRequiresSessionId(endpoint).should.be.true;
});
});
[
'/session',
'/session/',
'/sessions',
'/appium/sessions',
'/status',
].forEach(function (endpoint) {
it(`should be false with ${endpoint}`, function () {
j.endpointRequiresSessionId(endpoint).should.be.false;
});
});
});
});
@@ -1,6 +1,4 @@
// transpile:mocha
import {_} from 'lodash';
import _ from 'lodash';
import {METHOD_MAP, routeToCommandName} from '../../../lib/protocol';
import crypto from 'crypto';
@@ -43,7 +41,7 @@ describe('Protocol', function () {
}
let hash = shasum.digest('hex').substring(0, 8);
// Modify the hash whenever the protocol has intentionally been modified.
hash.should.equal('6e93ad90');
hash.should.equal('00fcb083');
});
});
@@ -53,11 +51,6 @@ describe('Protocol', function () {
cmdName.should.equal('timeouts');
});
it('should properly lookup correct command name for endpoint with session', function () {
const cmdName = routeToCommandName('/timeouts/implicit_wait', 'POST');
cmdName.should.equal('implicitWait');
});
it('should properly lookup correct command name for endpoint without session', function () {
const cmdName = routeToCommandName('/status', 'GET');
cmdName.should.equal('getStatus');
+34
View File
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.1](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.0...@appium/base-plugin@2.3.1) (2025-02-19)
**Note:** Version bump only for package @appium/base-plugin
## [2.3.0](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.52...@appium/base-plugin@2.3.0) (2025-01-08)
### Features
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
## [2.2.52](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.51...@appium/base-plugin@2.2.52) (2025-01-06)
**Note:** Version bump only for package @appium/base-plugin
## [2.2.51](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.50...@appium/base-plugin@2.2.51) (2025-01-02)
### Bug Fixes
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
## [2.2.50](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.49...@appium/base-plugin@2.2.50) (2024-12-05)
**Note:** Version bump only for package @appium/base-plugin
+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.1",
"description": "The base plugin used to create Appium 2.0 plugins",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.13.1",
"@appium/support": "^6.0.0"
"@appium/base-driver": "^9.16.0",
"@appium/support": "^6.0.4"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
@@ -1,4 +1,4 @@
import BasePlugin from '../../lib/plugin';
import {BasePlugin} from '../../lib/plugin';
describe('base plugin', function () {
let should;
+49
View File
@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.30](https://github.com/appium/appium/compare/@appium/docutils@1.0.29...@appium/docutils@1.0.30) (2025-02-19)
### Bug Fixes
* **docutils:** update dependency consola to v3.4.0 ([#20908](https://github.com/appium/appium/issues/20908)) ([8fb0da3](https://github.com/appium/appium/commit/8fb0da34bb58c44c30ac8900a62510f1fcb663bc))
* **docutils:** update dependency typescript to v5.7.3 ([#20893](https://github.com/appium/appium/issues/20893)) ([6706a00](https://github.com/appium/appium/commit/6706a0037eda5f7eb9eb2a4b0fb49888618c12c2))
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [1.0.29](https://github.com/appium/appium/compare/@appium/docutils@1.0.28...@appium/docutils@1.0.29) (2025-01-08)
**Note:** Version bump only for package @appium/docutils
## [1.0.28](https://github.com/appium/appium/compare/@appium/docutils@1.0.27...@appium/docutils@1.0.28) (2025-01-06)
### Bug Fixes
* **support:** update dependency teen_process to v2.2.3 ([#20882](https://github.com/appium/appium/issues/20882)) ([488e5de](https://github.com/appium/appium/commit/488e5dea82d3c525acf483952570e32f319f3722))
## [1.0.27](https://github.com/appium/appium/compare/@appium/docutils@1.0.26...@appium/docutils@1.0.27) (2025-01-02)
### Bug Fixes
* **docutils:** update dependency consola to v3.3.0 ([#20851](https://github.com/appium/appium/issues/20851)) ([aa88083](https://github.com/appium/appium/commit/aa880837da3bdbbc531e9d4c13b8baa76573cb7e))
* **docutils:** update dependency consola to v3.3.1 ([#20855](https://github.com/appium/appium/issues/20855)) ([861d060](https://github.com/appium/appium/commit/861d060713dade7b6c55b2b0d49e97c6e9e66103))
* **docutils:** update dependency consola to v3.3.3 ([#20858](https://github.com/appium/appium/issues/20858)) ([4b3a8c2](https://github.com/appium/appium/commit/4b3a8c2197a587ec5a2606b662e4e7667a7852cb))
* **docutils:** update dependency yaml to v2.7.0 ([#20870](https://github.com/appium/appium/issues/20870)) ([389a3e8](https://github.com/appium/appium/commit/389a3e83ac0069504338f7182a4e938cd391de7e))
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
## [1.0.26](https://github.com/appium/appium/compare/@appium/docutils@1.0.25...@appium/docutils@1.0.26) (2024-12-05)
+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);
});

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