Compare commits

...
Author SHA1 Message Date
Jonathan Lipps 7c05f5c213 chore: publish
- appium@2.17.0
 - @appium/base-driver@9.16.3
 - @appium/base-plugin@2.3.4
 - @appium/docutils@1.0.33
 - @appium/driver-test-support@0.7.7
 - @appium/execute-driver-plugin@4.0.3
 - @appium/fake-driver@5.7.2
 - @appium/fake-plugin@3.2.4
 - @appium/images-plugin@3.1.0
 - @appium/plugin-test-support@0.3.52
 - @appium/storage-plugin@0.1.0
 - @appium/support@6.0.7
 - @appium/test-support@3.1.7
 - @appium/tsconfig@0.3.5
 - @appium/types@0.25.2
 - @appium/universal-xml-plugin@1.0.31
2025-03-11 14:40:58 -07:00
Mykola Mokhnach e68757b349 fix(base-driver): Fix proxy url generation (#21099) 2025-03-11 19:27:57 +01:00
renovate[bot] f007b46a65 chore(deps): update eslint-related packages (#21097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-11 10:28:26 +00:00
Edgars Eglītis f9b51ec526 docs: add storage-plugin documentation (#21096)
* chore: fix typo

* chore: list storage-plugin as a known plugin

* docs: align storage-plugin docs with other plugins

* chore: address comments
2025-03-10 19:45:28 +02:00
renovate[bot] 884173f967 chore(docutils): update dependency mkdocs-git-revision-date-localized-plugin to v1.4.4 (#21093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 16:37:11 +00:00
Mykola Mokhnach f3b1f3556c chore(storage-plugin): Simplify add to storage logic (#21092) 2025-03-09 20:12:10 +01:00
Sakura Nene ef3d781d88 docs(appium): fix a broken character that caused incorrect rendering of mkdocs (#21090) (#21091) 2025-03-09 17:28:08 +01:00
Mykola Mokhnach 15280b80d2 feat(storage-plugin): Tune the files keeping behaviour (#21086) 2025-03-09 09:34:32 +01:00
renovate[bot] 365a7b1770 chore(deps): update eslint-related packages to v9.22.0 (#21085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 22:49:45 +00:00
Kazuaki Matsuo d1e7124d4d docs(base-driver): remove outdated READMEs (#21083) 2025-03-08 09:18:25 -08:00
renovate[bot] cb799d7ab8 chore(deps): update dependency @types/node to v22.13.10 (#21084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 13:24:36 +00:00
Mykola Mokhnach ba4aa394d1 feat: Add storage plugin (#21075) 2025-03-08 09:21:10 +01:00
Kazuaki Matsuo a4eda0054b chore(fake-driver): accept 3.0.0-beta.0 as well (#21076) 2025-03-07 18:51:49 -08:00
renovate[bot] dbd0d321e0 chore(deps): update dependency eslint-config-prettier to v10.1.1 (#21081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 14:39:41 +00:00
renovate[bot] c6d5383425 chore(support): update dependency axios to v1.8.2 (#21080)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 10:40:52 +00:00
Mykola Mokhnach 859f3c430a test(images-plugin): Fix test host (#21079) 2025-03-07 09:03:54 +01:00
Mykola Mokhnach e3560e0bee test: Update tests of images plugin (#21078) 2025-03-07 07:56:28 +01:00
renovate[bot] 51eea37e93 chore(deps): update dependency finalhandler to v2 (#21074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 14:21:47 +01:00
renovate[bot] 053c105f6f chore(types): update dependency type-fest to v4.37.0 (#21073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 14:21:16 +01:00
kkb912002 0c50504266 feat(images-plugin): supports image elements included in actions. (#21055) 2025-03-06 14:20:55 +01:00
renovate[bot] 1766a8d0cf chore(docutils): update dependency mkdocs-git-revision-date-localized-plugin to v1.4.1 (#21071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 11:40:34 +00:00
renovate[bot] dbad59f1ff chore(docutils): update dependency typescript to v5.8.2 (#21057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 06:34:24 +00:00
renovate[bot] b70ddd50a8 chore(deps): update definitelytyped (#21067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 02:11:37 +00:00
renovate[bot] bbe04a7ac0 chore(deps): update dependency typescript-eslint to v8.26.0 (#21068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 22:16:07 +00:00
renovate[bot] a7c9f6198a chore(workflows): update peter-evans/create-pull-request action to v7.0.8 (#21066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 16:30:05 +00:00
renovate[bot] c5723e7655 chore(tsconfig): update dependency @tsconfig/node14 to v14.1.3 (#21065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 10:59:02 +00:00
renovate[bot] 0b1b1a4776 chore(deps): update dependency lerna to v8.2.1 (#21064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 19:34:32 +00:00
renovate[bot] d98665ab20 chore(deps): update dependency @types/node to v22.13.9 (#21063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 15:55:39 +00:00
renovate[bot] 2171aaa000 chore(deps): update dependency prettier to v3.5.3 (#21060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 20:04:08 +00:00
renovate[bot] a3196e3108 chore(docutils): update dependency mkdocs-material to v9.6.7 (#21061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 16:02:51 +00:00
renovate[bot] aeba9fd2d6 chore(universal-xml-plugin): update dependency @xmldom/xmldom to v0.9.8 (#21056)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 18:00:03 +02:00
renovate[bot] 97c4d05be8 chore(support): update dependency teen_process to v2.3.1 (#21058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 14:33:47 +00:00
renovate[bot] c4d8ab45c3 chore(types): update dependency type-fest to v4.36.0 (#21059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 10:42:37 +00:00
renovate[bot] 63e4eccb4a chore(docutils): update dependency mkdocs-material to v9.6.6 (#21054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 15:07:37 +00:00
renovate[bot] 1178eef6b8 chore(deps): update definitelytyped (#21053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 09:52:40 +00:00
renovate[bot] e67e210df2 chore(deps): update dependency eslint-config-prettier to v10.0.2 (#21050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 15:57:52 +00:00
renovate[bot] d68fe942be chore(universal-xml-plugin): update dependency fast-xml-parser to v5.0.8 (#21051)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 14:05:56 +02:00
renovate[bot] dcd6b8f141 chore(support): update dependency axios to v1.8.1 (#21049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-26 22:34:36 +00:00
renovate[bot] c8ad6c05f5 chore(execute-driver-plugin): update dependency webdriverio to v9.10.1 (#21048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-26 18:00:12 +00:00
Mykola Mokhnach 554114203f fix(base-driver): Update parseCapsArray function types (#21045) 2025-02-25 21:33:08 +01:00
Mykola Mokhnach 594bc04c03 fix(base-driver): Tune capabilities array parsing (#21044) 2025-02-25 20:58:54 +01:00
renovate[bot] ca8590221e chore(deps): update dependency typescript-eslint to v8.25.0 (#21040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 19:37:26 +00:00
renovate[bot] 4b75153659 chore(workflows): update peter-evans/create-pull-request action to v7.0.7 (#21039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 13:29:21 +00:00
renovate[bot] 2d9282bf22 chore(execute-driver-plugin): update dependency webdriverio to v9.10.0 (#21036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-23 09:06:08 +01:00
Mykola Mokhnach 0d1150943d chore(base-driver): Implement proper requests cancellation (#21035) 2025-02-23 09:05:50 +01:00
Mykola Mokhnach b9a586d336 chore(base-driver): Add types to proxy options (#21034) 2025-02-23 08:09:27 +01:00
renovate[bot] 7a4fbb2ab3 chore(deps): update eslint-related packages to v9.21.0 (#21033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 21:43:44 +00:00
renovate[bot] e7ac01ca3d chore(deps): update definitelytyped (#21031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 17:37:00 +00:00
renovate[bot] 53a0520887 chore(deps): update dependency prettier to v3.5.2 (#21032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 14:49:33 +00:00
renovate[bot] 0228ffafb8 chore(appium): update dependency ws to v8.18.1 (#21030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 10:56:44 +00:00
Mykola Mokhnach 25acb30e9a chore(base-driver): Refactor command name detection in JWProxy (#21029) 2025-02-22 08:02:56 +01:00
Edgars Eglītis dbd2e51556 chore(docs): remove sauce from docs footer (#21019) 2025-02-21 00:35:04 -08:00
renovate[bot] 0bfe9d69de chore(deps): update dependency lerna to v8.2.0 (#21010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 03:07:33 +00:00
Jonathan Lipps e473b05130 chore: publish
- appium@2.16.2
 - @appium/base-driver@9.16.2
 - @appium/base-plugin@2.3.3
 - @appium/docutils@1.0.32
 - @appium/fake-plugin@3.2.3
 - @appium/images-plugin@3.0.30
 - @appium/support@6.0.6
 - @appium/test-support@3.1.6
 - @appium/universal-xml-plugin@1.0.30
2025-02-20 15:56:38 -08:00
Mykola Mokhnach dd64a48fca fix(base-driver): Restore the legacy proxy url behaviour (#21021) 2025-02-20 15:24:46 -08:00
renovate[bot] 4473ef4da8 chore(support): update dependency uuid to v11.1.0 (#21012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 23:09:25 +01:00
renovate[bot] f9f88e66b6 chore(universal-xml-plugin): update dependency fast-xml-parser to v5 (#21013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 23:09:09 +01:00
Jonathan Lipps 715fd03c45 chore: publish
- appium@2.16.1
 - @appium/base-driver@9.16.1
 - @appium/base-plugin@2.3.2
 - @appium/docutils@1.0.31
 - @appium/driver-test-support@0.7.6
 - @appium/fake-plugin@3.2.2
 - @appium/images-plugin@3.0.29
 - @appium/plugin-test-support@0.3.51
 - @appium/schema@0.8.1
 - @appium/support@6.0.5
 - @appium/test-support@3.1.5
 - @appium/tsconfig@0.3.4
 - @appium/types@0.25.1
 - @appium/universal-xml-plugin@1.0.29
2025-02-20 11:04:05 -08:00
Mykola Mokhnach 8a664a4eef fix(base-driver): Optimize the logic of getUrlForProxy (#21018) 2025-02-20 19:59:15 +01:00
renovate[bot] dc24864cbb chore(deps): update dependency eslint-import-resolver-typescript to v3.8.3 (#21016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 12:59:27 +00:00
Edgars Eglītis c102d8fd8e fix(renovate): use chore prefix for all PRs (#21014) 2025-02-20 12:59:44 +02:00
renovate[bot] 703085bc54 chore(docutils): update dependency mkdocs-material to v9.6.5 (#21009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 09:08:09 +00:00
Edgars Eglītis 47497bc8da chore(schema,tsconfig,universal-xml-plugin): fix repository field in package.json (#21011) 2025-02-20 10:04:01 +02:00
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
123 changed files with 7325 additions and 4355 deletions
+5
View File
@@ -53,6 +53,11 @@ labels:
matcher:
files: ['packages/base-plugin/**']
- label: '@appium/storage-plugin'
sync: true
matcher:
files: ['packages/storage-plugin/**']
- label: '@appium/docutils'
sync: true
matcher:
@@ -29,7 +29,7 @@ jobs:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_DOCS_PROJECT_ID }}
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.6
uses: peter-evans/create-pull-request@v7.0.8
with:
token: ${{ github.token }}
commit-message: 'docs(appium): Update documentation translations'
+1
View File
@@ -30,6 +30,7 @@ to driver/client documentation
* [opencv](packages/opencv/CHANGELOG.md)
* [plugin-test-support](packages/plugin-test-support/CHANGELOG.md)
* [schema](packages/schema/CHANGELOG.md)
* [storage-plugin](packages/storage-plugin/CHANGELOG.md)
* [strongbox](packages/strongbox/CHANGELOG.md)
* [support](packages/support/CHANGELOG.md)
* [test-support](packages/test-support/CHANGELOG.md)
-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/).
+3717 -3251
View File
File diff suppressed because it is too large Load Diff
+44 -21
View File
@@ -85,42 +85,65 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/js": "9.17.0",
"@tsconfig/node14": "14.1.2",
"@types/chai": "5.0.1",
"@types/chai-as-promised": "8.0.1",
"@types/diff": "6.0.0",
"@eslint/js": "9.22.0",
"@tsconfig/node14": "14.1.3",
"@types/argparse": "2.0.17",
"@types/archiver": "6.0.3",
"@types/async-lock": "1.4.2",
"@types/base64-stream": "1.0.5",
"@types/bluebird": "3.5.42",
"@types/chai": "5.2.0",
"@types/chai-as-promised": "8.0.2",
"@types/diff": "7.0.1",
"@types/express": "5.0.0",
"@types/jsftp": "2.1.5",
"@types/json-schema": "7.0.15",
"@types/klaw": "3.0.6",
"@types/lockfile": "1.0.4",
"@types/lodash": "4.17.16",
"@types/method-override": "3.0.0",
"@types/mocha": "10.0.10",
"@types/node": "22.10.5",
"@types/mv": "2.1.4",
"@types/ncp": "2.0.8",
"@types/node": "22.13.10",
"@types/pluralize": "0.0.33",
"@types/semver": "7.5.8",
"@types/sinon": "17.0.3",
"@types/serve-favicon": "2.5.7",
"@types/shell-quote": "1.7.5",
"@types/sinon": "17.0.4",
"@types/sinon-chai": "4.0.0",
"@types/source-map-support": "0.5.10",
"@types/supports-color": "8.1.3",
"@types/teen_process": "2.0.4",
"@types/ws": "8.5.13",
"@types/uuid": "10.0.0",
"@types/which": "3.0.4",
"@types/wrap-ansi": "3.0.0",
"@types/ws": "8.18.0",
"@types/xmldom": "0.1.34",
"@types/yargs": "17.0.33",
"asyncbox": "3.0.0",
"chai": "5.1.2",
"chai": "5.2.0",
"chai-as-promised": "8.0.1",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "5.0.0",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.7.0",
"cross-env": "7.0.3",
"eslint": "9.22.0",
"eslint-config-prettier": "10.1.1",
"eslint-import-resolver-typescript": "3.8.4",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-mocha": "10.5.0",
"eslint-plugin-promise": "7.2.1",
"finalhandler": "1.3.1",
"finalhandler": "2.1.0",
"get-port": "5.1.1",
"json-schema-to-typescript": "15.0.3",
"lerna": "8.1.9",
"json-schema-to-typescript": "15.0.4",
"lerna": "8.2.1",
"log-symbols": "4.1.0",
"midnight-smoker": "8.0.0",
"mjpeg-consumer": "2.0.0",
"mjpeg-server": "0.3.1",
"mocha": "11.0.1",
"mocha": "11.1.0",
"npm-run-all2": "6.2.6",
"prettier": "3.4.2",
"prettier": "3.5.3",
"rewiremock": "3.14.5",
"rimraf": "5.0.10",
"serve-static": "1.16.2",
@@ -128,11 +151,11 @@
"sync-monorepo-packages": "1.0.2",
"ts-node": "10.9.2",
"tsd": "0.31.2",
"typescript": "5.7.2",
"typescript-eslint": "8.19.0",
"typescript": "5.8.2",
"typescript-eslint": "8.26.1",
"validate.js": "0.13.1",
"webdriverio": "9.5.1",
"ws": "8.18.0",
"webdriverio": "9.10.1",
"ws": "8.18.1",
"yaml-js": "0.3.1"
},
"engines": {
+60
View File
@@ -3,6 +3,66 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.17.0](https://github.com/appium/appium/compare/appium@2.16.2...appium@2.17.0) (2025-03-11)
### Features
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
## [2.16.2](https://github.com/appium/appium/compare/appium@2.16.1...appium@2.16.2) (2025-02-20)
**Note:** Version bump only for package appium
## [2.16.1](https://github.com/appium/appium/compare/appium@2.16.0...appium@2.16.1) (2025-02-20)
**Note:** Version bump only for package appium
## [2.16.0](https://github.com/appium/appium/compare/appium@2.15.0...appium@2.16.0) (2025-02-19)
### Features
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
### Bug Fixes
* **appium:** add await for async function ([#20974](https://github.com/appium/appium/issues/20974)) ([21e2aab](https://github.com/appium/appium/commit/21e2aabc391fefe0dff8e29fe1cd6be908d5b3d0))
* **appium:** Wait for upstream web socket to open before sending data to it ([#20953](https://github.com/appium/appium/issues/20953)) ([3cfe3af](https://github.com/appium/appium/commit/3cfe3af1c8d88c8ec79d2d89c7e76d697205c86b))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.3 ([#20895](https://github.com/appium/appium/issues/20895)) ([8741474](https://github.com/appium/appium/commit/874147449a26bbec03b08e7e95fb9e4c6b5881af))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.4 ([#20898](https://github.com/appium/appium/issues/20898)) ([7f91d5c](https://github.com/appium/appium/commit/7f91d5cba850100124088d774ba8704e3765863f))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.7 ([#20903](https://github.com/appium/appium/issues/20903)) ([c60fdc8](https://github.com/appium/appium/commit/c60fdc8d847b4aeed2d8b8307180194ee32e5ca9))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.0 ([#20930](https://github.com/appium/appium/issues/20930)) ([5780982](https://github.com/appium/appium/commit/57809826a410b71dca155f59e081f77dcaf671ed))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.1 ([#20934](https://github.com/appium/appium/issues/20934)) ([50fa5d6](https://github.com/appium/appium/commit/50fa5d670bd967200a78e5b4b8a54d5c9b85901b))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.2 ([#20944](https://github.com/appium/appium/issues/20944)) ([f5e0819](https://github.com/appium/appium/commit/f5e0819f505b55421b7d260f69a41ae980390a91))
* **execute-driver-plugin:** update dependency webdriverio to v9.8.0 ([#20964](https://github.com/appium/appium/issues/20964)) ([ce32226](https://github.com/appium/appium/commit/ce32226cb57fb3f07bf4862dbbfcba227ff1230c))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.0 ([#20994](https://github.com/appium/appium/issues/20994)) ([b3c877f](https://github.com/appium/appium/commit/b3c877f66ddf3ccf7a25ca1f7a3952c660bd7dda))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.1 ([#20996](https://github.com/appium/appium/issues/20996)) ([c6a080f](https://github.com/appium/appium/commit/c6a080f7ade03a49a274ce6fbc84629406fdc2a7))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.3 ([#21005](https://github.com/appium/appium/issues/21005)) ([686a630](https://github.com/appium/appium/commit/686a630fbdf525db16b42fdfc1bbdaf6426b41d0))
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [2.15.0](https://github.com/appium/appium/compare/appium@2.14.1...appium@2.15.0) (2025-01-08)
-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`**
@@ -65,3 +65,25 @@ Find a a list of all UI elements matching a given a locator strategy and a selec
`any`
A possibly-empty list of element objects
### `performActions`
`POST` **`/session/:sessionId/actions`**
If the actions contains image elements as origin, convert them to viewport coordinates before sending it to the external driver
**`See`**
[https://w3c.github.io/webdriver/#perform-actions](https://w3c.github.io/webdriver/#perform-actions)
<!-- comment source: method-signature -->
#### Parameters
| Name | Type | Description |
| :------ | :------ | :------ |
| `actions` | `ActionSequence[]` | an array of action sequences |
#### Response
`null`
@@ -21,4 +21,5 @@ The command listings can be found here:
* [Execute Driver Plugin](./execute-driver-plugin.md)
* [Images Plugin](./images-plugin.md)
* [Relaxed Caps Plugin](./relaxed-caps-plugin.md)
* [Storage Plugin](./storage-plugin.md)
* [Universal XML Plugin](./universal-xml-plugin.md)
@@ -0,0 +1,113 @@
# Plugin: storage
!!! tip
All these commands can be invoked without creating a session, allowing you to
prepare your test environment in advance.
### `addStorageItem`
`POST` **`/storage/add`**
Add a new file to the storage
#### Parameters
| Name | Type | Description |
| :------ | :------ | :------ |
| `name` | `string` | the name used to save the file (must not include path separator characters) |
| `sha1` | `string` | SHA1 hash of the file to be uploaded |
#### Example
```bash
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}' http://127.0.0.1:4723/storage/add
```
#### Response
`AddRequestResult`
A JSON object in the following format:
```json
{
"ws": {
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
},
"ttlMs": 300000
}
```
| Name | Type | Description |
| :------ | :------ | :------ |
| `ws.stream` | `string` | the pathname of the streaming web socket used to upload the file content |
| `ws.events` | `string` | the pathname of the events web socket used to notify about upload success or a failure |
| `ttlMs` | `number` | the amount of milliseconds both web sockets will be kept active before they expire, or a file payload would be successfully uploaded |
### `listStorageItems`
`GET` **`/storage/list`**
List all files present in the storage
#### Example
```bash
curl http://127.0.0.1:4723/storage/list
```
#### Response
`List<StorageItem>`
A list of items, where each item has the following properties:
| Name | Type | Description |
| :------ | :------ | :------ |
| `name` | `string` | the name of the file in the storage |
| `path` | `string` | full path to the file on the remote file system |
| `size` | `number` | file size in bytes |
### `deleteStorageItem`
`POST` **`/storage/delete`**
Deletes a file in the storage with the specified name
#### Parameters
| Name | Type | Description |
| :------ | :------ | :------ |
| `name` | `string` | the name of the file to be deleted |
#### Example
```bash
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa"}' http://127.0.0.1:4723/storage/delete
```
#### Response
`boolean`
`false` if the file does not exist in the storage, or `true` upon successful file deletion
### `resetStorage`
`POST` **`/storage/reset`**
Deletes all uploaded files and stops any incomplete uploads.
If the `APPIUM_STORAGE_KEEP_ALL` flag is enabled, all uploaded files will be preserved,
and only the incomplete uploads will be stopped.
#### Example
```bash
curl -X POST http://127.0.0.1:4723/storage/reset
```
#### Response
`undefined`
@@ -32,6 +32,7 @@ These clients are not maintained by the Appium team and can be used with other l
|[Nightwatch.js](https://nightwatchjs.org/guide/mobile-app-testing/introduction.html)|Node.js|
|[RobotFramework](https://github.com/serhatbolsu/robotframework-appiumlibrary)|DSL|
|[multicatch's appium-client](https://github.com/multicatch/appium-client)|Rust|
|[SwiftAppium](https://github.com/milcgroup/swiftappium)|Swift|
In general, any W3C WebDriver spec-compatible client will also integrate well with Appium, though
some Appium-specific commands may not be implemented in other clients.
@@ -44,8 +44,6 @@ These drivers are not maintained by the Appium team and can be used to target ad
|[Tizen](https://github.com/Samsung/appium-tizen-driver)|`--source=npm appium-tizen-driver`|Android|Native|Community / Samsung|
|[TizenTV](https://github.com/headspinio/appium-tizen-tv-driver)|`--source=npm appium-tizen-tv-driver`|Samsung TV|Web|HeadSpin|
|[Youi](https://github.com/YOU-i-Labs/appium-youiengine-driver)|`--source=npm appium-youiengine-driver`|iOS, Android, macOS, Linux, tvOS|Native|Community / You.i|
|[axe UiAutomator2](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-uiautomator2-driver`|Android|Native|[Deque](https://www.deque.com)|
|[axe XCUITest](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-xcuitest-driver`|iOS|Native|[Deque](https://www.deque.com)|
!!! note
@@ -23,6 +23,7 @@ These plugins are are currently maintained by the Appium team:
|[Execute Driver](https://github.com/appium/appium/tree/master/packages/execute-driver-plugin)|`execute-driver`|Run entire batches of commands in a single call to the Appium server|
|[Images](https://github.com/appium/appium/tree/master/packages/images-plugin)|`images`|Image matching and comparison features|
|[Relaxed Caps](https://github.com/appium/appium/tree/master/packages/relaxed-caps-plugin)|`relaxed-caps`|Relax Appium's requirement for vendor prefixes on capabilities|
|[Storage](https://github.com/appium/appium/tree/master/packages/storage-plugin)|`storage`|Server-side storage with client-side management|
|[Universal XML](https://github.com/appium/appium/tree/master/packages/universal-xml-plugin)|`universal-xml`|Instead of the standard XML format for iOS and Android, use an XML definition that is the same across both platforms|
### Other Plugins
+3 -3
View File
@@ -73,9 +73,9 @@ Appium supports [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol
The actual behavior depends on individual drivers while the Appium and the baseーdriver support the protocol.
Please make sure if a driver supports the protocol and what kind of commands/events it supports in the documentation.
| Capability Name | Type | Description |
|---|---|−--|
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
| Capability Name | Type | Description |
|-----------------|-----------|-----------------------------------------|
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
## Using `appium:options` to Group Capabilities
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
by the use of the `appium:eventTimings` capability (set it to `true` to log event
timings).
With this capability turned on, the `GET /session/:id` response (i.e., the
response to `driver.getSessionDetails()` or similar, depending on client) will
be decorated with an `events` property. This is the structure of that `events`
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
the response to `driver.logs.events` or similar, depending on client) will be
decorated with an `events` property. This is the structure of that `events`
property:
```
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
command, and so on.
You can only receive data about events that have happened when you make the
call to `/session/:id`, so the best time to get data about an entire session is
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
right before quitting it.
The Appium team maintains an event timings parser tool that can be used to
generate various kinds of reports from event timings output:
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
!!! note
In the past, events were available as a part of `GET /session/:id` response
## Add a custom event
!!! warning "TODO"
-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
+1
View File
@@ -46,6 +46,7 @@ nav:
- commands/execute-driver-plugin.md
- commands/images-plugin.md
- commands/relaxed-caps-plugin.md
- commands/storage-plugin.md
- commands/universal-xml-plugin.md
- Guides:
- Migration:
@@ -4,8 +4,4 @@
<a href="https://www.browserstack.com/browserstack-appium?utm_campaigncode=701OW00000AoUTQYA3&utm_medium=partnered&utm_source=appium">
<img src="/docs/en/latest/assets/images/sponsor-logo-browserstack-dark.png" />
</a>
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner">
<img src="/docs/en/latest/assets/images/sponsor-logo-sauce.png" />
</a>
</div>
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
by the use of the `appium:eventTimings` capability (set it to `true` to log event
timings).
With this capability turned on, the `GET /session/:id` response (i.e., the
response to `driver.getSessionDetails()` or similar, depending on client) will
be decorated with an `events` property. This is the structure of that `events`
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
the response to `driver.logs.events` or similar, depending on client) will be
decorated with an `events` property. This is the structure of that `events`
property:
```
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
command, and so on.
You can only receive data about events that have happened when you make the
call to `/session/:id`, so the best time to get data about an entire session is
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
right before quitting it.
The Appium team maintains an event timings parser tool that can be used to
generate various kinds of reports from event timings output:
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
!!! note
In the past, events were available as a part of `GET /session/:id` response
## Add a custom event
!!! warning "TODO"
+30 -35
View File
@@ -8,6 +8,8 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
promoteAppiumOptions,
promoteAppiumOptionsForObject,
generateDriverLogPrefix,
@@ -22,7 +24,8 @@ import {
import {util} from '@appium/support';
import {getDefaultsForExtension} from './schema';
import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
import * as bidiHelpers from './bidi';
import * as bidiCommands from './bidi-commands';
import * as inspectorCommands from './inspector-commands';
const desiredCapabilityConstraints = /** @type {const} */ ({
automationName: {
@@ -192,6 +195,11 @@ class AppiumDriver extends DriverCore {
}));
}
async getAppiumSessions () {
throw new errors.NotImplementedError('Not implemented yet. ' +
'Please check https://github.com/appium/appium/issues/20880 for more details.');
}
printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
this.log.info(
driverVersion
@@ -403,7 +411,7 @@ class AppiumDriver extends DriverCore {
);
// set the New Command Timeout for the inner driver
driverInstance.startNewCommandTimeout();
await driverInstance.startNewCommandTimeout();
// apply initial values to Appium settings (if provided)
if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
@@ -426,7 +434,7 @@ class AppiumDriver extends DriverCore {
if (dCaps.webSocketUrl) {
const {address, port, basePath} = this.args;
const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
const host = bidiHelpers.determineBiDiHost(address);
const host = bidiCommands.determineBiDiHost(address);
const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
this.log.info(
`Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
@@ -565,32 +573,6 @@ class AppiumDriver extends DriverCore {
}
}
/**
* @param {string} sessionId
*/
cleanupBidiSockets(sessionId) {
// clean up any bidi sockets associated with session
if (this.bidiSockets[sessionId]) {
try {
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
for (const ws of this.bidiSockets[sessionId]) {
// 1001 means server is going away
ws.close(1001, 'Appium session is closing');
}
} catch {}
delete this.bidiSockets[sessionId];
const proxyClient = this.bidiProxyClients[sessionId];
if (proxyClient) {
this.log.debug(`Also closing proxy connection to upstream bidi server`);
try {
// 1000 means normal closure, which seems correct when Appium is acting as the client
proxyClient.close(1000);
} catch {}
delete this.bidiProxyClients[sessionId];
}
}
}
async deleteAllSessions(opts = {}) {
const sessionsCount = _.size(this.sessions);
if (0 === sessionsCount) {
@@ -935,15 +917,28 @@ class AppiumDriver extends DriverCore {
return dstSession && dstSession.canProxy(sessionId);
}
onBidiConnection = bidiHelpers.onBidiConnection;
onBidiMessage = bidiHelpers.onBidiMessage;
onBidiServerError = bidiHelpers.onBidiServerError;
onBidiConnection = bidiCommands.onBidiConnection;
onBidiMessage = bidiCommands.onBidiMessage;
onBidiServerError = bidiCommands.onBidiServerError;
cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
listCommands = inspectorCommands.listCommands;
listExtensions = inspectorCommands.listExtensions;
}
// help decide which commands should be proxied to sub-drivers and which
// should be handled by this, our umbrella driver
/**
* Help decide which commands should be proxied to sub-drivers and which
* should be handled by this, our umbrella driver
* @param {string} cmd
* @returns {boolean}
*/
function isAppiumDriverCommand(cmd) {
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
return !isSessionCommand(cmd)
|| _.includes([
DELETE_SESSION_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
], cmd);
}
/**
@@ -176,6 +176,38 @@ export function onBidiServerError(this: AppiumDriver, err: Error): void {
this.log.warn(`Error from bidi websocket server: ${err}`);
}
/**
* Clean up any bidi sockets associated with session
*
* @param sessionId
*/
export function cleanupBidiSockets(this: AppiumDriver, sessionId: string): void {
if (!this.bidiSockets[sessionId]) {
return;
}
try {
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
for (const ws of this.bidiSockets[sessionId]) {
// 1001 means server is going away
ws.close(1001, 'Appium session is closing');
}
} catch {}
delete this.bidiSockets[sessionId];
const proxyClient = this.bidiProxyClients[sessionId];
if (!proxyClient) {
return;
}
this.log.debug(`Also closing proxy connection to upstream bidi server`);
try {
// 1000 means normal closure, which seems correct when Appium is acting as the client
proxyClient.close(1000);
} catch {}
delete this.bidiProxyClients[sessionId];
}
// #region Private functions
/**
* Initialize a new bidi connection
* @param ws The websocket connection object
@@ -263,6 +295,7 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
const socketSend = B.promisify(socket.send, {context: socket});
return async (data: string | Buffer) => {
try {
await assertIsOpen(socket);
await socketSend(data);
} catch (err) {
logSocketErr(err);
@@ -468,3 +501,43 @@ function initBidiEventListeners(
plugin.eventEmitter?.on(BIDI_EVENT_NAME, eventListenerFactory('plugin', plugin));
}
}
async function assertIsOpen(
ws: WebSocket,
timeoutMs: number = 5000,
): Promise<WebSocket> {
if (ws.readyState === ws.OPEN) {
return ws;
}
if (ws.readyState > ws.OPEN) {
throw new Error(`The BiDi web socket at ${ws.url} is not open`);
}
let errorListener;
let openListener;
// The socket is in CONNECTING state. Wait up to `timeoutMs` until it is open
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(
new Error(
`The BiDi web socket at ${ws.url} did not ` +
`open after ${timeoutMs}ms timeout`
)
), timeoutMs);
ws.once('error', reject);
errorListener = reject;
ws.once('open', resolve);
openListener = resolve;
});
} finally {
if (errorListener) {
ws.off('error', errorListener);
}
if (openListener) {
ws.off('open', openListener);
}
}
return ws;
}
// #endregion
+1
View File
@@ -35,6 +35,7 @@ export const KNOWN_PLUGINS = Object.freeze(
images: '@appium/images-plugin',
'execute-driver': '@appium/execute-driver-plugin',
'relaxed-caps': '@appium/relaxed-caps-plugin',
storage: '@appium/storage-plugin',
'universal-xml': '@appium/universal-xml-plugin',
}),
);
+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});
+12 -18
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.15.0",
"version": "2.17.0",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,40 +60,34 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.15.0",
"@appium/base-plugin": "^2.3.0",
"@appium/docutils": "^1.0.29",
"@appium/base-driver": "^9.16.3",
"@appium/base-plugin": "^2.3.4",
"@appium/docutils": "^1.0.33",
"@appium/logger": "^1.6.1",
"@appium/schema": "^0.7.1",
"@appium/support": "^6.0.3",
"@appium/types": "^0.24.0",
"@appium/schema": "^0.8.1",
"@appium/support": "^6.0.7",
"@appium/types": "^0.25.2",
"@sidvind/better-ajv-errors": "3.0.1",
"@types/argparse": "2.0.17",
"@types/bluebird": "3.5.42",
"@types/semver": "7.5.8",
"@types/teen_process": "2.0.4",
"@types/wrap-ansi": "3.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"argparse": "2.0.1",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.7.9",
"axios": "1.8.2",
"bluebird": "3.7.2",
"cross-env": "7.0.3",
"lilconfig": "3.1.3",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
"ora": "5.4.1",
"package-changed": "3.0.0",
"resolve-from": "5.0.0",
"semver": "7.6.3",
"semver": "7.7.1",
"source-map-support": "0.5.21",
"teen_process": "2.2.3",
"type-fest": "4.31.0",
"teen_process": "2.3.1",
"type-fest": "4.37.0",
"winston": "3.17.0",
"wrap-ansi": "7.0.0",
"ws": "8.18.0",
"ws": "8.18.1",
"yaml": "2.7.0"
},
"engines": {
@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "9.5.1"
"webdriverio": "9.10.1"
}
}
@@ -208,6 +208,77 @@ describe('FakeDriver via HTTP', function () {
});
});
describe('inspector commands', function () {
withServer();
let driver;
beforeEach(async function () {
driver = await wdio({...wdOpts, capabilities: caps});
});
afterEach(async function () {
if (driver) {
await driver.deleteSession();
driver = null;
}
});
it('should list available driver commands', async function () {
driver.addCommand(
'listCommands',
async () => (await axios.get(
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/commands`
)).data.value
);
const commands = await driver.listCommands();
JSON.stringify(commands.rest.base['/session/:sessionId/frame'])
.should.eql(JSON.stringify({POST: {command: 'setFrame', params: [
{name: 'id', required: true}
]}}));
_.size(commands.rest.driver).should.be.greaterThan(1);
JSON.stringify(commands.bidi.base.session.subscribe).should.eql(
JSON.stringify({
command: 'bidiSubscribe',
'params': [
{
name: 'events',
required: true
},
{
name: 'contexts',
required: false
}
]
})
);
_.size(commands.bidi.base).should.be.greaterThan(1);
_.size(commands.bidi.driver).should.be.greaterThan(0);
});
it('should list available driver extensions', async function () {
driver.addCommand(
'listExtensions',
async () => (await axios.get(
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/extensions`
)).data.value
);
const extensions = await driver.listExtensions();
JSON.stringify(extensions.rest.driver['fake: setThing']).should.eql(
JSON.stringify({
command: 'setFakeThing',
params: [{
name: 'thing',
required: true
}]
})
);
_.size(extensions.rest.driver).should.be.greaterThan(1);
});
});
describe('session handling', function () {
withServer();
+1
View File
@@ -8,6 +8,7 @@ export default {
denyInsecure: [],
driversImportChunkSize: 3,
keepAliveTimeout: 600,
requestTimeout: 3600,
localTimezone: false,
logFormat: 'text',
loglevel: 'debug',
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.16.3](https://github.com/appium/appium/compare/@appium/base-driver@9.16.2...@appium/base-driver@9.16.3) (2025-03-11)
### Bug Fixes
* **base-driver:** Fix proxy url generation ([#21099](https://github.com/appium/appium/issues/21099)) ([e68757b](https://github.com/appium/appium/commit/e68757b3493a5b0f961f7136c0ae6e857d806f09))
* **base-driver:** Tune capabilities array parsing ([#21044](https://github.com/appium/appium/issues/21044)) ([594bc04](https://github.com/appium/appium/commit/594bc04c03fb073cd7ad31d7e23f77fb8041b92e))
* **base-driver:** Update parseCapsArray function types ([#21045](https://github.com/appium/appium/issues/21045)) ([5541142](https://github.com/appium/appium/commit/554114203fbe26f303337f049f942e046c815074))
## [9.16.2](https://github.com/appium/appium/compare/@appium/base-driver@9.16.1...@appium/base-driver@9.16.2) (2025-02-20)
### Bug Fixes
* **base-driver:** Restore the legacy proxy url behaviour ([#21021](https://github.com/appium/appium/issues/21021)) ([dd64a48](https://github.com/appium/appium/commit/dd64a48fca047a728ad109d525bc4787daa75edc))
## [9.16.1](https://github.com/appium/appium/compare/@appium/base-driver@9.16.0...@appium/base-driver@9.16.1) (2025-02-20)
### Bug Fixes
* **base-driver:** Optimize the logic of getUrlForProxy ([#21018](https://github.com/appium/appium/issues/21018)) ([8a664a4](https://github.com/appium/appium/commit/8a664a4eef97f45c5bdf7f19708149488b63d7fc))
## [9.16.0](https://github.com/appium/appium/compare/@appium/base-driver@9.15.0...@appium/base-driver@9.16.0) (2025-02-19)
### Features
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
* **base-driver:** Print the closest match if the given script did not match ([#20956](https://github.com/appium/appium/issues/20956)) ([f8b5799](https://github.com/appium/appium/commit/f8b57999e5a4a13a089cff12954de5d99c8a35fc))
### Bug Fixes
* **driver-test-support:** update definitelytyped ([#20942](https://github.com/appium/appium/issues/20942)) ([f7c63ee](https://github.com/appium/appium/commit/f7c63ee5d87be21eba577a4cfd0b0a08a050afeb))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [9.15.0](https://github.com/appium/appium/compare/@appium/base-driver@9.14.1...@appium/base-driver@9.15.0) (2025-01-08)
-8
View File
@@ -10,14 +10,6 @@ directly as it does nothing on its own. Instead, you should extend this driver w
*own* Appium drivers. Check out the [Building Drivers](https://appium.io/docs/en/latest/developing/build-drivers/)
documentation for more details.
Each included utility is documented in its own README:
* [BaseDriver](lib/basedriver)
* [The Appium Express Server](lib/express)
* [The Mobile JSON Wire Protocol Encapsulation](lib/mjsonwp)
* [The JSONWP Proxy Library](lib/jsonwp-proxy)
* [The JSONWP Status Library](lib/jsonwp-status)
## License
Apache-2.0
@@ -1,36 +0,0 @@
appium-base-driver
===================
This is the parent class that all [appium](appium.io) drivers inherit from. Appium drivers themselves can either be started from the command line as standalone appium servers, or can be included by another module (appium) which then proxies commands to the appropriate driver based on [Desired Capabilities](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md).
An appium driver is a module which processes [Mobile Json Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) commands and controls a device accordingly. The commands can either come in over HTTP as json api requests, or they can be passed to the driver object programmatically as already-parsed json object (without the HTTP headers and junk).
The appium Base driver already includes the [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md) module, which is the HTTP server that converts incoming requests into json objects that get sent to the driver programmatically.
The appium Base driver already has all the REST api routes, validation, and error codes supplied by [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md).
Appium drivers are designed to have a *single testing session* per instantiation. This means that one Driver object should be attached to a single device and handle commands from a single client. The main appium driver handles multiple sessions and instantiates a new instance of the desired driver for each new session.
## Writing your own appium driver
Writing your own appium driver starts with inheriting and extending this Base driver module.
Appium Base driver has some properties that all drivers share:
- `driver.opts` - these are the options passed into the driver constructor. Your driver's constructor should take an object of options and pass it on the the Base driver by calling `super(opts)` in your constructor.
- `driver.desiredCapConstraints` - Base driver sets this property with a customer `setter` function so that when you create a driver, you can add an object which defines the validation contraints of which desired capabilities your new driver can handle. Of course each driver will have it's own specific desired capabilities. Look for examples on our other drivers.
- `driver.createSession(caps)` - this is the function which gets desired capabilities and creates a session. Make sure to call `super.createSession(caps)` so that things like `this.sessionId` and `this.caps` are populated, and the caps are validated against your `desiredCapConstraints`.
- `driver.caps` - these are the desired capabilities for the current session.
- `driver.sessionId` - this is the ID of the current session. It gets populated automaticall by `baseDriver.createSession`.
- `driver.proxyReqRes()` - used by mjsonwp module for proxying http commands to another process (like chromedriver or selendroid)
- `driver.jwpProxyAvoid` - used by mjsonwp module. You can specify what REST api routes which you want to SKIP the automatic proxy to another server (which is optional) and instead be handled by your driver.
Base driver exposes an event called `onUnexpectedShutdown` which is called when the driver is shut down unexpectedly (usually after invocation of the `startUnexpectedShutdown` method).
Your driver should also implement a startUnexpectedShutdown method?
@@ -10,6 +10,7 @@ import {
} from '@appium/types';
import {mixin} from './mixin';
import {BaseDriver} from '../driver';
import {distance} from 'fastest-levenshtein';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -26,9 +27,33 @@ const ExecuteCommands: IExecuteCommands = {
const commandMetadata = {...Driver.executeMethodMap?.[script]};
if (!commandMetadata.command) {
const availableScripts = _.keys(Driver.executeMethodMap);
if (_.isEmpty(availableScripts)) {
throw new errors.UnsupportedOperationError(
`Unsupported execute method '${script}'. ` +
`Make sure the installed ${Driver.name} is up-to-date. ` +
`The current driver version does not define any execute methods.`
);
}
const matchesMap: StringRecord<string[]> = availableScripts
.map((name) => [distance(script, name), name])
.reduce((acc, [key, value]) => {
if (key in acc) {
acc[key].push(value);
} else {
acc[key] = [value];
}
return acc;
}, {});
const sortedMatches = _.flatten(
_.keys(matchesMap)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.map((x) => matchesMap[x])
);
throw new errors.UnsupportedOperationError(
`Unsupported execute method '${script}'. Available methods ` +
`are: ${availableScripts.join(', ')}`
`Unsupported execute method '${script}', did you mean '${sortedMatches[0]}'? ` +
`Make sure the installed ${Driver.name} is up-to-date. ` +
`Execute methods available in the current driver version are: ` +
sortedMatches.join(', ')
);
}
const args = validateExecuteMethodParams(protoArgs, commandMetadata.params);
@@ -16,6 +16,7 @@ import {
type InitialOpts,
type DefaultDeleteSessionResult,
type SingularSessionData,
type SessionCapabilities,
} from '@appium/types';
import B from 'bluebird';
import _ from 'lodash';
@@ -359,6 +360,8 @@ export class BaseDriver<
/**
* Returns capabilities for the session and event history (if applicable)
* @deprecated Use {@linkcode ISessionCommands.getAppiumSessionCapabilities} instead for getting the capabilities.
* Use {@linkcode EventCommands.getLogEvents} instead to get the event history.
*/
async getSession() {
return (
@@ -366,6 +369,13 @@ export class BaseDriver<
) as SingularSessionData<C, SessionData>;
}
/**
* Returns capabilities for the session
*/
async getAppiumSessionCapabilities(): Promise<SessionCapabilities<C>> {
return {capabilities: this.caps};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async deleteSession(sessionId?: string | null) {
await this.clearNewCommandTimeout();
+17 -13
View File
@@ -373,29 +373,33 @@ export function duplicateKeys(input, firstKey, secondKey) {
}
/**
* Takes a desired capability and tries to JSON.parse it as an array,
* Takes a capability value and tries to JSON.parse it as an array,
* and either returns the parsed array or a singleton array.
*
* @param {string|Array<String>} cap A desired capability
* @param {string|string[]} capValue Capability value
* @returns {string[]}
*/
export function parseCapsArray(cap) {
if (_.isArray(cap)) {
return cap;
export function parseCapsArray(capValue) {
if (_.isArray(capValue)) {
return capValue;
}
let parsedCaps;
try {
parsedCaps = JSON.parse(cap);
if (_.isArray(parsedCaps)) {
return parsedCaps;
const parsed = JSON.parse(capValue);
if (_.isArray(parsed)) {
return parsed;
}
} catch (e) {
logger.warn(`Failed to parse capability as JSON array: ${e.message}`);
const message = `Failed to parse capability as JSON array: ${e.message}`;
if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
throw new TypeError(message);
}
logger.warn(message);
}
if (_.isString(cap)) {
return [cap];
if (_.isString(capValue)) {
return [capValue];
}
throw new Error(`must provide a string or JSON Array; received ${cap}`);
throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
}
/**
@@ -1,59 +0,0 @@
## appium-express
[Express](http://expressjs.com/) server tuned for to serve [Appium](http://appium.io/).
### Configuration
The `appium-express` server comes configured with:
1. appropriate logging formats
2. service of necessary static assets
3. allowance of cross-domain requests
4. default error handling
5. fix for invalid content types sent by certain clients
To configure routes, a function that takes an Express server is passed into the
server. This function can add whatever routes are wanted.
### Usage
```js
import { server } from 'appium-base-driver';
// configure the routes
function configureRoutes (app) {
app.get('/hello', (req, res) => {
res.header['content-type'] = 'text/html';
res.status(200).send('Hello');
});
app.get('/world', (req, res) => {
res.header['content-type'] = 'text/html';
res.status(200).send('World');
});
}
const port = 5000;
const host = 'localhost';
const appiumServer = await server({
routeConfiguringFunction,
port,
host,
});
```
## Watch
```
npm run watch
```
## Test
```
npm test
```
+24 -6
View File
@@ -76,7 +76,7 @@ async function createServer (app, cliArgs) {
* @param {ServerOpts} opts
* @returns {Promise<AppiumServer>}
*/
async function server(opts) {
export async function server(opts) {
const {
routeConfiguringFunction,
port,
@@ -87,6 +87,7 @@ async function server(opts) {
extraMethodMap = {},
serverUpdaters = [],
keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS,
requestTimeout,
} = opts;
const app = express();
@@ -123,7 +124,13 @@ async function server(opts) {
// want to block extensions' ability to add routes if they want.
app.all('*', catch404Handler);
await startServer({httpServer, hostname, port, keepAliveTimeout});
await startServer({
httpServer,
hostname,
port,
keepAliveTimeout,
requestTimeout,
});
resolve(appiumServer);
} catch (err) {
@@ -136,7 +143,7 @@ async function server(opts) {
* Sets up some Express middleware and stuff
* @param {ConfigureServerOpts} opts
*/
function configureServer({
export function configureServer({
app,
addRoutes,
allowCors = true,
@@ -267,13 +274,22 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
* @param {StartServerOpts} opts
* @returns {Promise<void>}
*/
async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
async function startServer({
httpServer,
port,
hostname,
keepAliveTimeout,
requestTimeout,
}) {
// If the hostname is omitted, the server will accept
// connections on any IP address
/** @type {(port: number, hostname?: string) => B<http.Server>} */
const start = B.promisify(httpServer.listen, {context: httpServer});
const startPromise = start(port, hostname);
httpServer.keepAliveTimeout = keepAliveTimeout;
if (_.isInteger(requestTimeout)) {
httpServer.requestTimeout = Number(requestTimeout);
}
// headers timeout must be greater than keepAliveTimeout
httpServer.headersTimeout = keepAliveTimeout + 5 * 1000;
await startPromise;
@@ -284,7 +300,7 @@ async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
* @param {string} basePath
* @returns {string}
*/
function normalizeBasePath(basePath) {
export function normalizeBasePath(basePath) {
if (!_.isString(basePath)) {
throw new Error(`Invalid path prefix ${basePath}`);
}
@@ -302,7 +318,6 @@ function normalizeBasePath(basePath) {
return basePath;
}
export {server, configureServer, normalizeBasePath};
/**
* Options for {@linkcode startServer}.
@@ -311,6 +326,8 @@ export {server, configureServer, normalizeBasePath};
* @property {number} port - Port to run on
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
* @property {string} [hostname] - Optional hostname
* @property {number} [requestTimeout] - The timeout value in milliseconds for
* receiving the entire request from the client
*/
/**
@@ -344,6 +361,7 @@ export {server, configureServer, normalizeBasePath};
* @property {MethodMap} [extraMethodMap]
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
* @property {number} [keepAliveTimeout]
* @property {number} [requestTimeout]
*/
/**
+3
View File
@@ -49,6 +49,9 @@ export {
// Web socket helpers
export {DEFAULT_WS_PATHNAME_PREFIX} from './express/websocket';
// BiDi exports
export {BIDI_COMMANDS} from './protocol/bidi-commands';
export {generateDriverLogPrefix} from './basedriver/helpers';
/**
@@ -1,52 +0,0 @@
## appium-jsonwp-proxy
Proxy middleware for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md). Allows
### Usage
The proxy is used by instantiating with the details of the Selenium server to which to proxy. The options for the constructor are passed as a hash with the following possible members:
- `scheme` - defaults to 'http'
- `server` - defaults to 'localhost'
- `port` - defaults to `4444`
- `base` - defaults to ''
- `sessionId` - the session id of the session on the remote server
- `reqBasePath` - the base path of the server which the request was originally sent to (defaults to '')
Once the proxy is created, there are two `async` methods:
`command (url, method, body)`
Sends a "command" to the proxied server, using the "url", which is the endpoing, with the HTTP method and optional body.
```js
import { JWProxy } from 'appium-base-driver';
let host = 'my.host.com';
let port = 4445;
let proxy = new JWProxy({server: host, port: port});
// get the Selenium server status
let seStatus = await proxy.command('/status', 'GET');
```
`proxyReqRes (req, res)`
Proxies a request and response to the proxied server. Used to handle the entire conversation of a request/response cycle.
```js
import { JWProxy } from 'appium-base-driver';
import http from 'http';
let host = 'my.host.com';
let port = 4445;
let proxy = new JWProxy({server: host, port: port});
http.createServer(function (req, res) {
await proxy.proxyReqRes(res, res);
}).listen(9615);
```
@@ -43,6 +43,11 @@ const {MJSONWP, W3C} = PROTOCOLS;
const DEFAULT_LOG = logger.getLogger('Protocol Converter');
class ProtocolConverter {
/**
*
* @param {ProxyFunction} proxyFunc
* @param {import('@appium/types').AppiumLogger | null} [log=null]
*/
constructor(proxyFunc, log = null) {
this.proxyFunc = proxyFunc;
this._downstreamProtocol = null;
@@ -67,7 +72,7 @@ class ProtocolConverter {
* provided in the request, we need to do 3 proxies and combine the result
*
* @param {Object} body Request body
* @return {Array} Array of W3C + MJSONWP compatible timeout objects
* @return {Object[]} Array of W3C + MJSONWP compatible timeout objects
*/
getTimeoutRequestObjects(body) {
if (this.downstreamProtocol === W3C && _.has(body, 'ms') && _.has(body, 'type')) {
@@ -99,9 +104,10 @@ class ProtocolConverter {
/**
* Proxy an array of timeout objects and merge the result
* @param {String} url Endpoint url
* @param {String} method Endpoint method
* @param {Object} body Request body
* @param {string} url Endpoint url
* @param {string} method Endpoint method
* @param {import('@appium/types').HTTPBody} body Request body
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxySetTimeouts(url, method, body) {
let response, resBody;
@@ -127,9 +133,16 @@ class ProtocolConverter {
// ...Otherwise, continue to the next timeouts call
}
return [response, resBody];
return [/** @type {import('@appium/types').ProxyResponse} */(response), resBody];
}
/**
*
* @param {string} url
* @param {string} method
* @param {import('@appium/types').HTTPBody} body
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxySetWindow(url, method, body) {
const bodyObj = util.safeJsonParse(body);
if (_.isPlainObject(bodyObj)) {
@@ -160,6 +173,13 @@ class ProtocolConverter {
return await this.proxyFunc(url, method, body);
}
/**
*
* @param {string} url
* @param {string} method
* @param {import('@appium/types').HTTPBody} body
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxySetValue(url, method, body) {
const bodyObj = util.safeJsonParse(body);
if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj.text) || util.hasValue(bodyObj.value))) {
@@ -186,6 +206,13 @@ class ProtocolConverter {
return await this.proxyFunc(url, method, body);
}
/**
*
* @param {string} url
* @param {string} method
* @param {import('@appium/types').HTTPBody} body
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxySetFrame(url, method, body) {
const bodyObj = util.safeJsonParse(body);
return _.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)
@@ -196,6 +223,13 @@ class ProtocolConverter {
: await this.proxyFunc(url, method, body);
}
/**
*
* @param {string} url
* @param {string} method
* @param {import('@appium/types').HTTPBody} body
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxyPerformActions(url, method, body) {
const bodyObj = util.safeJsonParse(body);
return _.isPlainObject(bodyObj)
@@ -207,6 +241,12 @@ class ProtocolConverter {
: await this.proxyFunc(url, method, body);
}
/**
*
* @param {string} url
* @param {string} method
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxyReleaseActions(url, method) {
return await this.proxyFunc(url, method);
}
@@ -218,8 +258,8 @@ class ProtocolConverter {
* @param {string} commandName
* @param {string} url
* @param {string} method
* @param {?string|object} body
* @returns The proxyfying result as [response, responseBody] tuple
* @param {import('@appium/types').HTTPBody} [body]
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async convertAndProxy(commandName, url, method, body) {
if (!this.downstreamProtocol) {
@@ -272,3 +312,7 @@ class ProtocolConverter {
}
export default ProtocolConverter;
/**
* @typedef {(url: string, method: string, body?: import('@appium/types').HTTPBody) => Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>} ProxyFunction
*/
@@ -0,0 +1,52 @@
import axios from 'axios';
import { CancellationError } from 'bluebird';
import EventEmitter from 'node:events';
const CANCEL_EVENT = 'cancel';
const FINISH_EVENT = 'finish';
export class ProxyRequest {
private readonly _requestConfig: axios.RawAxiosRequestConfig;
private readonly _ee: EventEmitter;
private _resultPromise: Promise<any> | null;
constructor(requestConfig: axios.RawAxiosRequestConfig<any>) {
this._requestConfig = requestConfig;
this._ee = new EventEmitter();
this._resultPromise = null;
}
async execute(): Promise<axios.AxiosResponse> {
if (this._resultPromise) {
return await this._resultPromise;
}
try {
this._resultPromise = Promise.race([
this._makeRacingTimer(),
this._makeRequest(),
]);
return await this._resultPromise;
} finally {
this._ee.emit(FINISH_EVENT);
this._ee.removeAllListeners();
}
}
cancel(): void {
this._ee.emit(CANCEL_EVENT);
}
private async _makeRequest(): Promise<axios.AxiosResponse> {
return await axios(this._requestConfig);
}
private async _makeRacingTimer(): Promise<void> {
return await new Promise((resolve, reject) => {
this._ee.once(FINISH_EVENT, resolve);
this._ee.once(CANCEL_EVENT, () => reject(new CancellationError(
'The request has been cancelled'
)));
});
}
}
+153 -78
View File
@@ -1,6 +1,5 @@
import _ from 'lodash';
import {logger, util} from '@appium/support';
import axios from 'axios';
import {getSummaryByCode} from '../jsonwp-status/status';
import {
errors,
@@ -9,15 +8,19 @@ import {
errorFromW3CJsonCode,
getResponseForW3CError,
} from '../protocol/errors';
import {routeToCommandName} from '../protocol';
import {isSessionCommand, routeToCommandName} from '../protocol';
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
import ProtocolConverter from './protocol-converter';
import {formatResponseValue, formatStatus} from '../protocol/helpers';
import http from 'http';
import https from 'https';
import { match as pathToRegexMatch } from 'path-to-regexp';
import nodeUrl from 'node:url';
import { ProxyRequest } from './proxy-request';
const DEFAULT_LOG = logger.getLogger('WD Proxy');
const DEFAULT_REQUEST_TIMEOUT = 240000;
const COMMAND_WITH_SESSION_ID_MATCHER = pathToRegexMatch('{/*prefix}/session/:sessionId{/*command}');
const {MJSONWP, W3C} = PROTOCOLS;
@@ -33,7 +36,7 @@ const ALLOWED_OPTS = [
'keepAlive',
];
class JWProxy {
export class JWProxy {
/** @type {string} */
scheme;
/** @type {string} */
@@ -48,13 +51,20 @@ class JWProxy {
sessionId;
/** @type {number} */
timeout;
/** @type {Protocol | null | undefined} */
_downstreamProtocol;
/** @type {ProxyRequest[]} */
_activeRequests;
/**
* @param {import('@appium/types').ProxyOptions} [opts={}]
*/
constructor(opts = {}) {
opts = _.pick(opts, ALLOWED_OPTS);
const filteredOpts = _.pick(opts, ALLOWED_OPTS);
// omit 'log' in the defaults assignment here because 'log' is a getter and we are going to set
// it to this._log (which lies behind the getter) further down
const options = _.defaults(_.omit(opts, 'log'), {
/** @type {import('@appium/types').ProxyOptions} */
const options = _.defaults(_.omit(filteredOpts, 'log'), {
scheme: 'http',
server: 'localhost',
port: 4444,
@@ -63,7 +73,7 @@ class JWProxy {
sessionId: null,
timeout: DEFAULT_REQUEST_TIMEOUT,
});
options.scheme = options.scheme.toLowerCase();
options.scheme = /** @type {string} */ (options.scheme).toLowerCase();
Object.assign(this, options);
this._activeRequests = [];
@@ -77,6 +87,8 @@ class JWProxy {
this.httpsAgent = new https.Agent(agentOpts);
this.protocolConverter = new ProtocolConverter(this.proxy.bind(this), opts.log);
this._log = opts.log;
this.log.debug(`${this.constructor.name} options: ${JSON.stringify(options)}`);
}
get log() {
@@ -93,27 +105,32 @@ class JWProxy {
* @returns {Promise<import('axios').AxiosResponse>}
*/
async request(requestConfig) {
const reqPromise = axios(requestConfig);
this._activeRequests.push(reqPromise);
const req = new ProxyRequest(requestConfig);
this._activeRequests.push(req);
try {
return await reqPromise;
return await req.execute();
} finally {
_.pull(this._activeRequests, reqPromise);
_.pull(this._activeRequests, req);
}
}
/**
* @returns {number}
*/
getActiveRequestsCount() {
return this._activeRequests.length;
}
cancelActiveRequests() {
for (const ar of this._activeRequests) {
ar.cancel();
}
this._activeRequests = [];
}
endpointRequiresSessionId(endpoint) {
return !_.includes(['/session', '/sessions', '/status'], endpoint);
}
/**
* @param {Protocol | null | undefined} value
*/
set downstreamProtocol(value) {
this._downstreamProtocol = value;
this.protocolConverter.downstreamProtocol = value;
@@ -123,67 +140,46 @@ class JWProxy {
return this._downstreamProtocol;
}
getUrlForProxy(url) {
if (url === '') {
url = '/';
/**
*
* @param {string} url
* @param {string} [method]
* @returns {string}
*/
getUrlForProxy(url, method) {
const parsedUrl = this._parseUrl(url);
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
const commandName = normalizedPathname
? routeToCommandName(
normalizedPathname,
/** @type {import('@appium/types').HTTPMethod | undefined} */ (method)
)
: '';
const requiresSessionId = !commandName || (commandName && isSessionCommand(commandName));
const proxyPrefix = `${this.scheme}://${this.server}:${this.port}${this.base}`;
let proxySuffix = normalizedPathname ? `/${_.trimStart(normalizedPathname, '/')}` : '';
if (parsedUrl.search) {
proxySuffix += parsedUrl.search;
}
const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
const endpointRe = '(/(session|status))';
let remainingUrl = '';
if (/^http/.test(url)) {
const first = new RegExp(`(https?://.+)${endpointRe}`).exec(url);
if (!first) {
throw new Error('Got a complete url but could not extract JWP endpoint');
}
remainingUrl = url.replace(first[1], '');
} else if (new RegExp('^/').test(url)) {
remainingUrl = url;
} else {
throw new Error(`Did not know what to do with url '${url}'`);
if (!requiresSessionId) {
return `${proxyPrefix}${proxySuffix}`;
}
const stripPrefixRe = new RegExp('^.*?(/(session|status).*)$');
if (stripPrefixRe.test(remainingUrl)) {
remainingUrl = /** @type {RegExpExecArray} */ (stripPrefixRe.exec(remainingUrl))[1];
if (!this.sessionId) {
throw new ReferenceError(`Session ID is not set, but saw a URL that requires it (${url})`);
}
if (!new RegExp(endpointRe).test(remainingUrl)) {
remainingUrl = `/session/${this.sessionId}${remainingUrl}`;
}
const requiresSessionId = this.endpointRequiresSessionId(remainingUrl);
if (requiresSessionId && this.sessionId === null) {
throw new Error('Trying to proxy a session command without session id');
}
const sessionBaseRe = new RegExp('^/session/([^/]+)');
if (sessionBaseRe.test(remainingUrl)) {
if (this.sessionId === null) {
throw new ReferenceError(
`Session ID is not set, but saw a URL path referencing a session (${remainingUrl}). This may be a bug in your client.`
);
}
// we have something like /session/:id/foobar, so we need to replace
// the session id
const match = sessionBaseRe.exec(remainingUrl);
// TODO: if `requiresSessionId` is `false` and `sessionId` is `null`, this is a bug.
// are we sure `sessionId` is not `null`?
remainingUrl = remainingUrl.replace(
/** @type {RegExpExecArray} */ (match)[1],
/** @type {string} */ (this.sessionId)
);
} else if (requiresSessionId) {
throw new Error(`Could not find :session section for url: ${remainingUrl}`);
}
remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes
return proxyBase + remainingUrl;
return `${proxyPrefix}/session/${this.sessionId}${proxySuffix}`;
}
/**
*
* @param {string} url
* @param {string} method
* @param {import('@appium/types').HTTPBody} [body=null]
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxy(url, method, body = null) {
method = method.toUpperCase();
const newUrl = this.getUrlForProxy(url);
const newUrl = this.getUrlForProxy(url, method);
const truncateBody = (content) =>
_.truncate(_.isString(content) ? content : JSON.stringify(content), {
length: MAX_LOG_BODY_LENGTH,
@@ -252,8 +248,12 @@ class JWProxy {
// Some servers, like chromedriver may return response code 200 for non-zero JSONWP statuses
throwProxyError(data);
}
const res = {statusCode: status, headers, body: data};
return [res, data];
const headersMap = /** @type {import('@appium/types').HTTPHeaders} */ (headers);
return [{
statusCode: status,
headers: headersMap,
body: data,
}, data];
} catch (e) {
// We only consider an error unexpected if this was not
// an async request module error or if the response cannot be cast to
@@ -276,6 +276,11 @@ class JWProxy {
}
}
/**
*
* @param {Record<string, any>} resObj
* @returns {Protocol | undefined}
*/
getProtocolFromResBody(resObj) {
if (_.isInteger(resObj.status)) {
return MJSONWP;
@@ -286,6 +291,7 @@ class JWProxy {
}
/**
* @deprecated This method is not used anymore and will be removed
*
* @param {string} url
* @param {import('@appium/types').HTTPMethod} method
@@ -319,10 +325,13 @@ class JWProxy {
*
* @param {string} url
* @param {import('@appium/types').HTTPMethod} method
* @param {any?} body
* @param {import('@appium/types').HTTPBody} [body=null]
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
*/
async proxyCommand(url, method, body = null) {
const commandName = this.requestToCommandName(url, method);
const parsedUrl = this._parseUrl(url);
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
const commandName = normalizedPathname ? routeToCommandName(normalizedPathname, method) : '';
if (!commandName) {
return await this.proxy(url, method, body);
}
@@ -335,8 +344,8 @@ class JWProxy {
*
* @param {string} url
* @param {import('@appium/types').HTTPMethod} method
* @param {any?} body
* @returns {Promise<unknown>}
* @param {import('@appium/types').HTTPBody} [body=null]
* @returns {Promise<import('@appium/types').HTTPBody>}
*/
async command(url, method, body = null) {
let response;
@@ -390,20 +399,40 @@ class JWProxy {
);
}
/**
*
* @param {string} url
* @returns {string | null}
*/
getSessionIdFromUrl(url) {
const match = url.match(/\/session\/([^/]+)/);
return match ? match[1] : null;
}
/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async proxyReqRes(req, res) {
// ! this method must not throw any exceptions
// ! make sure to call res.send before return
/** @type {number} */
let statusCode;
/** @type {import('@appium/types').HTTPBody} */
let resBodyObj;
try {
let response;
[response, resBodyObj] = await this.proxyCommand(req.originalUrl, req.method, req.body);
res.headers = response.headers;
[response, resBodyObj] = await this.proxyCommand(
req.originalUrl,
/** @type {import('@appium/types').HTTPMethod} */ (req.method),
req.body
);
for (const [name, value] of _.toPairs(response.headers)) {
if (!_.isNil(value)) {
res.setHeader(name, _.isBoolean(value) ? String(value) : value);
}
}
statusCode = response.statusCode;
} catch (err) {
[statusCode, resBodyObj] = getResponseForW3CError(
@@ -435,11 +464,57 @@ class JWProxy {
resBodyObj.value = formatResponseValue(resBodyObj.value);
res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
}
/**
*
* @param {string} url
* @returns {ParsedUrl}
*/
_parseUrl(url) {
const parsedUrl = nodeUrl.parse(url || '/');
if (
_.isNil(parsedUrl.href) || _.isNil(parsedUrl.pathname)
|| (parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
) {
throw new Error(`Did not know how to proxy the url '${url}'`);
}
return parsedUrl;
}
/**
*
* @param {ParsedUrl} parsedUrl
* @returns {string}
*/
_toNormalizedPathname(parsedUrl) {
if (!_.isString(parsedUrl.pathname)) {
return '';
}
let pathname = this.reqBasePath && parsedUrl.pathname.startsWith(this.reqBasePath)
? parsedUrl.pathname.replace(this.reqBasePath, '')
: parsedUrl.pathname;
const match = COMMAND_WITH_SESSION_ID_MATCHER(pathname);
// This is needed for the backward compatibility
// if drivers don't set reqBasePath properly
if (!this.reqBasePath) {
if (match && _.isArray(match.params?.prefix)) {
pathname = pathname.replace(`/${match.params?.prefix.join('/')}`, '');
} else if (_.startsWith(pathname, '/wd/hub')) {
pathname = pathname.replace('/wd/hub', '');
}
}
let result = pathname;
if (match) {
result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
}
return _.trimEnd(result, '/');
}
}
export {JWProxy};
export default JWProxy;
/**
* @typedef {Error & {response: {data: import('type-fest').JsonObject, status: import('http-status-codes').StatusCodes}}} ProxyError
* @typedef {nodeUrl.UrlWithStringQuery} ParsedUrl
* @typedef {typeof PROTOCOLS[keyof typeof PROTOCOLS]} Protocol
*/
@@ -1,20 +0,0 @@
## jsonwp-status
Library of status codes for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md).
### Usage
```
import { statusCodes } from 'appium-base-driver';
statusCodes.NoSuchContext;
// -> {code: 35, summary: 'No such context found'}
```
```
import { getSummaryByCode } from 'appium-base-driver';
getSummaryByCode(0);
// -> 'The command executed successfully.'
```
-100
View File
@@ -1,100 +0,0 @@
## webdriver and mobile-json-wire protocols
An abstraction of the Mobile JSON Wire Protocol ([spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)) and the W3C Wire Protocol ([spec](https://www.w3.org/TR/webdriver/) with Appium extensions (as specified [here](http://www.w3.org/TR/webdriver/#protocol-extensions)).
### Protocol Detection
In the event that a session is requested, and both MJSONWP _and_ W3C capabilities are provided, like this
```
{
"capabilities: {
"alwaysMatch": {...},
"firstMatch": [{...}, ...]
},
"desiredCapabilities": {...}
}
```
a W3C session will be served _unless_ the W3C Capabilities are incomplete. So if the `"desiredCapabilities"` object has more keys
then whatever the capabilities were matched for the W3C capabilities, then an MJSONWP session will be served instead
### Endpoints in the protocol
The Mobile JSON Wire Protocol package gives access to a number of endpoints documented [here](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md).
The W3C WebDriver Protocol package gives access to a number of endpoints documented in the [official documentation](https://www.w3.org/TR/webdriver/) and the
[simplified spec](https://github.com/jlipps/simple-wd-spec)
### Protocol
The basic class, subclassed by drivers that will use the protocol.
### routeConfiguringFunction (driver)
This function gives drivers access to the protocol routes. It returns a function that itself will take an [Express](http://expressjs.com/) application.
### isSessionCommand (command)
Checks if the `command` needs to have a session associated with it.
### ALL_COMMANDS
An array of all the commands that will be dispatched to by the Mobile JSON Wire Proxy endpoints.
### NO_SESSION_ID_COMMANDS
An array of commands that do not need a session associated with them.
### Errors
This package exports a number of classes and methods related to Selenium error handling. There are error classes for each Selenium error type (see [JSONWP Errors](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes), as well as the context errors in the [mobile spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md#webviews-and-other-contexts).
The list of errors, and their meanings, can be found [here for JSONWP](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/errors.md) and
[here for W3C Errors](https://www.w3.org/TR/webdriver/#handling-errors))
There are, in addition, two helper methods for dealing with errors
`isErrorType (err, type)`
- checks if the `err` object is a Mobile JSON Wire Protocol error of a particular type
- arguments
- `err` - the error object to test
- `type` - the error class to test against
- usage
```js
import { errors, isErrorType } from 'mobile-json-wire-protocol';
try {
// do some stuff...
} catch (err) {
if (isErrorType(err, errors.InvalidCookieDomainError)) {
// process...
}
}
```
`errorFromCode (code, message)`
- retrieve the appropriate error for an error code, with the supplied message.
- arguments
- `code` - the integer error code for a Mobile JSON Wire Protocol error
- `message` - the message to be encapsulated in the error
- usage
```js
import { errors, errorFromCode } from 'mobile-json-wire-protocol';
let error = errorFromCode(6, 'an error has occurred');
console.log(error instanceof errors.NoSuchDriverError);
// => true
console.log(error.message === 'an error has occurred');
// => true
```
@@ -3,7 +3,7 @@ const SUBSCRIPTION_REQUEST_PARAMS = /** @type {const} */ ({
optional: ['contexts'],
});
const BIDI_COMMANDS = /** @type {const} */ ({
export const BIDI_COMMANDS = /** @type {const} */ ({
session: {
subscribe: {
command: 'bidiSubscribe',
@@ -31,5 +31,3 @@ const BIDI_COMMANDS = /** @type {const} */ ({
// TODO add definitions for all bidi commands.
// spec link: https://w3c.github.io/webdriver-bidi/
export {BIDI_COMMANDS};
@@ -5,6 +5,8 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
makeArgs,
checkParams,
validateExecuteMethodParams,
@@ -30,4 +32,6 @@ export {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
LIST_DRIVER_COMMANDS_COMMAND,
LIST_DRIVER_EXTENSIONS_COMMAND,
};
@@ -16,9 +16,11 @@ import {isW3cCaps} from '../helpers/capabilities';
import log from '../basedriver/logger';
import { generateDriverLogPrefix } from '../basedriver/helpers';
const CREATE_SESSION_COMMAND = 'createSession';
const DELETE_SESSION_COMMAND = 'deleteSession';
const GET_STATUS_COMMAND = 'getStatus';
export const CREATE_SESSION_COMMAND = 'createSession';
export const DELETE_SESSION_COMMAND = 'deleteSession';
export const GET_STATUS_COMMAND = 'getStatus';
export const LIST_DRIVER_COMMANDS_COMMAND = 'listCommands';
export const LIST_DRIVER_EXTENSIONS_COMMAND = 'listExtensions';
/** @type {Set<string>} */
const deprecatedCommandsLogged = new Set();
@@ -559,9 +561,6 @@ export {
isSessionCommand,
driverShouldDoJwpProxy,
determineProtocol,
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
deprecatedCommandsLogged,
validateExecuteMethodParams,
};
+412 -367
View File
@@ -4,6 +4,12 @@ import _ from 'lodash';
import {util} from '@appium/support';
import {PROTOCOLS, DEFAULT_BASE_PATH} from '../constants';
import {match} from 'path-to-regexp';
import { LRUCache } from 'lru-cache';
/** @type {LRUCache<string, string>} */
const COMMAND_NAMES_CACHE = new LRUCache({
max: 1024,
});
const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
validate: (jsonObj) =>
@@ -22,6 +28,12 @@ const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
* @satisfies {import('@appium/types').MethodMap<import('../basedriver/driver').BaseDriver>}
*/
export const METHOD_MAP = /** @type {const} */ ({
//
// W3C WebDriver (and deprecated MJSONWP that will be removed in Appium 3)
// https://www.w3.org/TR/webdriver1/
// https://www.w3.org/TR/webdriver2/
//
'/status': {
GET: {command: 'getStatus'},
},
@@ -29,6 +41,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {
command: 'createSession',
payloadParams: {
// TODO: Appium 3 will accept only 'capabilities'.
validate: (jsonObj) =>
!jsonObj.capabilities &&
!jsonObj.desiredCapabilities &&
@@ -37,11 +50,8 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/sessions': {
GET: {command: 'getSessions'},
},
'/session/:sessionId': {
GET: {command: 'getSession'},
GET: {command: 'getSession', deprecated: true},
DELETE: {command: 'deleteSession'},
},
'/session/:sessionId/timeouts': {
@@ -59,7 +69,7 @@ export const METHOD_MAP = /** @type {const} */ ({
return 'W3C protocol expects any of script, pageLoad or implicit to be set';
}
} else {
// MJSONWP
// TODO: Remove in Appium 3
if (!util.hasValue(jsonObj.type) || !util.hasValue(jsonObj.ms)) {
return 'MJSONWP protocol requires type and ms';
}
@@ -69,25 +79,6 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/session/:sessionId/timeouts/async_script': {
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/timeouts/implicit_wait': {
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
},
// JSONWP
'/session/:sessionId/window_handle': {
GET: {command: 'getWindowHandle'},
},
// W3C
'/session/:sessionId/window/handle': {
GET: {command: 'getWindowHandle'},
},
// JSONWP
'/session/:sessionId/window_handles': {
GET: {command: 'getWindowHandles'},
},
// W3C
'/session/:sessionId/window/handles': {
GET: {command: 'getWindowHandles'},
},
@@ -104,39 +95,10 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/refresh': {
POST: {command: 'refresh'},
},
// MJSONWP
'/session/:sessionId/execute': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
// MJSONWP
'/session/:sessionId/execute_async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
'/session/:sessionId/screenshot': {
GET: {command: 'getScreenshot'},
},
'/session/:sessionId/ime/available_engines': {
GET: {command: 'availableIMEEngines', deprecated: true},
},
'/session/:sessionId/ime/active_engine': {
GET: {command: 'getActiveIMEEngine', deprecated: true},
},
'/session/:sessionId/ime/activated': {
GET: {command: 'isIMEActivated', deprecated: true},
},
'/session/:sessionId/ime/deactivate': {
POST: {command: 'deactivateIMEEngine', deprecated: true},
},
'/session/:sessionId/ime/activate': {
POST: {
command: 'activateIMEEngine',
payloadParams: {required: ['engine']},
deprecated: true,
},
},
'/session/:sessionId/frame': {
POST: {command: 'setFrame', payloadParams: {required: ['id']}},
},
@@ -148,6 +110,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {
command: 'setWindow',
payloadParams: {
// TODO: Appium 3 will only accept 'handle'. 'name' will be ginored.
optional: ['name', 'handle'],
// Return both values to match W3C and JSONWP protocols
makeArgs: (jsonObj) => {
@@ -167,16 +130,18 @@ export const METHOD_MAP = /** @type {const} */ ({
},
DELETE: {command: 'closeWindow'},
},
'/session/:sessionId/window/:windowhandle/size': {
GET: {command: 'getWindowSize', deprecated: true},
},
'/session/:sessionId/window/:windowhandle/position': {
POST: {deprecated: true},
GET: {deprecated: true},
},
'/session/:sessionId/window/:windowhandle/maximize': {
'/session/:sessionId/window/maximize': {
POST: {command: 'maximizeWindow'},
},
'/session/:sessionId/window/minimize': {
POST: {command: 'minimizeWindow'},
},
'/session/:sessionId/window/fullscreen': {
POST: {command: 'fullScreenWindow'},
},
'/session/:sessionId/window/new': {
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/cookie': {
GET: {command: 'getCookies'},
POST: {command: 'setCookie', payloadParams: {required: ['cookie']}},
@@ -206,7 +171,7 @@ export const METHOD_MAP = /** @type {const} */ ({
},
'/session/:sessionId/element/active': {
GET: {command: 'active'}, // W3C: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-get-active-element
POST: {command: 'active'},
POST: {command: 'active', deprecated: true},
},
'/session/:sessionId/element/:elementId': {
GET: {},
@@ -226,9 +191,6 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/click': {
POST: {command: 'click'},
},
'/session/:sessionId/element/:elementId/submit': {
POST: {command: 'submit', deprecated: true},
},
'/session/:sessionId/element/:elementId/text': {
GET: {command: 'getText'},
},
@@ -240,6 +202,7 @@ export const METHOD_MAP = /** @type {const} */ ({
!util.hasValue(jsonObj.value) &&
!util.hasValue(jsonObj.text) &&
'we require one of "text" or "value" params',
// TODO: Appium 3 will accept only 'value'.
optional: ['value', 'text'],
// override the default argument constructor because of the special
// logic here. Basically we want to accept either a value (old JSONWP)
@@ -250,9 +213,6 @@ export const METHOD_MAP = /** @type {const} */ ({
},
},
},
'/session/:sessionId/keys': {
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
},
'/session/:sessionId/element/:elementId/name': {
GET: {command: 'getName'},
},
@@ -268,21 +228,9 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/attribute/:name': {
GET: {command: 'getAttribute'},
},
'/session/:sessionId/element/:elementId/equals/:otherId': {
GET: {command: 'equalsElement', deprecated: true},
},
'/session/:sessionId/element/:elementId/displayed': {
GET: {command: 'elementDisplayed'},
},
'/session/:sessionId/element/:elementId/location': {
GET: {command: 'getLocation', deprecated: true},
},
'/session/:sessionId/element/:elementId/location_in_view': {
GET: {command: 'getLocationInView', deprecated: true},
},
'/session/:sessionId/element/:elementId/size': {
GET: {command: 'getSize', deprecated: true},
},
'/session/:sessionId/element/:elementId/shadow': {
GET: {command: 'elementShadowRoot'},
},
@@ -301,12 +249,8 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/element/:elementId/css/:propertyName': {
GET: {command: 'getCssProperty'},
},
'/session/:sessionId/orientation': {
GET: {command: 'getOrientation'},
POST: {
command: 'setOrientation',
payloadParams: {required: ['orientation']},
},
'/session/:sessionId/element/:elementId/property/:name': {
GET: {command: 'getProperty'},
},
// w3c v2 https://www.w3.org/TR/webdriver2/#get-computed-role
'session/:sessionId/element/:elementId/computedrole': {
@@ -316,10 +260,336 @@ export const METHOD_MAP = /** @type {const} */ ({
'session/:sessionId/element/:elementId/computedlabel': {
GET: {command: 'getComputedLabel'},
},
'/session/:sessionId/actions': {
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
DELETE: {command: 'releaseActions'},
},
'/session/:sessionId/alert/text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
'/session/:sessionId/alert/accept': {
POST: {command: 'postAcceptAlert'},
},
'/session/:sessionId/alert/dismiss': {
POST: {command: 'postDismissAlert'},
},
'/session/:sessionId/element/:elementId/rect': {
GET: {command: 'getElementRect'},
},
'/session/:sessionId/execute/sync': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
'/session/:sessionId/execute/async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
'/session/:sessionId/element/:elementId/screenshot': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/window/rect': {
GET: {command: 'getWindowRect'},
POST: {
command: 'setWindowRect',
payloadParams: {optional: ['x', 'y', 'width', 'height']},
},
},
//
// Appium specific
//
'/session/:sessionId/ime/available_engines': {
GET: {command: 'availableIMEEngines'},
},
'/session/:sessionId/ime/active_engine': {
GET: {command: 'getActiveIMEEngine'},
},
'/session/:sessionId/ime/activated': {
GET: {command: 'isIMEActivated'},
},
'/session/:sessionId/ime/deactivate': {
POST: {command: 'deactivateIMEEngine'},
},
'/session/:sessionId/ime/activate': {
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}},
},
'/session/:sessionId/rotation': {
GET: {command: 'getRotation'},
POST: {command: 'setRotation', payloadParams: {required: ['x', 'y', 'z']}},
},
'/session/:sessionId/location': {
GET: {command: 'getGeoLocation'},
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
},
'/session/:sessionId/orientation': {
GET: {command: 'getOrientation'},
POST: {
command: 'setOrientation',
payloadParams: {required: ['orientation']}
},
},
'/session/:sessionId/context': {
GET: {command: 'getCurrentContext'},
POST: {command: 'setContext', payloadParams: {required: ['name']}},
},
'/session/:sessionId/contexts': {
GET: {command: 'getContexts'},
},
'/session/:sessionId/network_connection': {
GET: {command: 'getNetworkConnection'},
POST: {
command: 'setNetworkConnection',
payloadParams: {unwrap: 'parameters', required: ['type']},
},
},
'/session/:sessionId/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
payloadParams: {required: ['status', 'value']},
},
},
'/appium/sessions': {
GET: {command: 'getAppiumSessions'},
},
'/session/:sessionId/appium/capabilities': {
GET: {command: 'getAppiumSessionCapabilities'}
},
'/session/:sessionId/appium/device/system_time': {
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
},
// #region Applications Management
'/session/:sessionId/appium/device/install_app': {
POST: {
command: 'installApp',
payloadParams: {
required: ['appPath'],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/activate_app': {
POST: {
command: 'activateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/remove_app': {
POST: {
command: 'removeApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/terminate_app': {
POST: {
command: 'terminateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/app_installed': {
POST: {
command: 'isAppInstalled',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
},
'/session/:sessionId/appium/device/app_state': {
GET: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
POST: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
deprecated: true,
},
},
// #endregion
'/session/:sessionId/appium/device/hide_keyboard': {
POST: {
command: 'hideKeyboard',
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
},
},
'/session/:sessionId/appium/device/is_keyboard_shown': {
GET: {command: 'isKeyboardShown'},
},
'/session/:sessionId/appium/device/push_file': {
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
},
'/session/:sessionId/appium/device/pull_file': {
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/pull_folder': {
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/settings': {
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
GET: {command: 'getSettings'},
},
'/session/:sessionId/appium/events': {
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/appium/log_event': {
POST: {
command: 'logCustomEvent',
payloadParams: {required: ['vendor', 'event']},
},
},
// #region Inspector
'/session/:sessionId/appium/commands': {
GET: {command: 'listCommands'},
},
'/session/:sessionId/appium/extensions': {
GET: {command: 'listExtensions'},
},
// #endregion
//
// 3rd party vendor/protcol support
//
// #region Selenium/Chromium browsers
'/session/:sessionId/se/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
'/session/:sessionId/se/log/types': {
GET: {command: 'getLogTypes'},
},
// #endregion
// #region chromium devtools
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
'/session/:sessionId/:vendor/cdp/execute': {
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
},
// #endregion
// #region Webauthn
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
'/session/:sessionId/webauthn/authenticator': {
POST: {
command: 'addVirtualAuthenticator',
payloadParams: {
required: ['protocol', 'transport'],
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
DELETE: {
command: 'removeVirtualAuthenticator',
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
POST: {
command: 'addAuthCredential',
payloadParams: {
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
optional: ['userHandle', 'signCount'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
GET: {command: 'getAuthCredential'},
DELETE: {command: 'removeAllAuthCredentials'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
DELETE: {command: 'removeAuthCredential'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
POST: {
command: 'setUserAuthVerified',
payloadParams: {
required: ['isUserVerified'],
},
},
},
// #endregion
//
// Endpoints deprecated entirely
//
// #region MJSONWP
'/sessions': {
GET: {command: 'getSessions', deprecated: true},
},
'/session/:sessionId/timeouts/async_script': {
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/timeouts/implicit_wait': {
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
},
'/session/:sessionId/window_handle': {
GET: {command: 'getWindowHandle', deprecated: true},
},
// Only 'window/handles' exists in W3C WebDriver spec.
'/session/:sessionId/window/handle': {
GET: {command: 'getWindowHandle', deprecated: true},
},
'/session/:sessionId/window_handles': {
GET: {command: 'getWindowHandles', deprecated: true},
},
'/session/:sessionId/execute': {
POST: {
command: 'execute',
payloadParams: {required: ['script', 'args']},
deprecated: true
},
},
'/session/:sessionId/execute_async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
deprecated: true
},
},
'/session/:sessionId/window/:windowhandle/size': {
GET: {command: 'getWindowSize', deprecated: true},
},
'/session/:sessionId/window/:windowhandle/position': {
POST: {deprecated: true},
GET: {deprecated: true},
},
'/session/:sessionId/window/:windowhandle/maximize': {
POST: {command: 'maximizeWindow', deprecated: true},
},
'/session/:sessionId/element/:elementId/submit': {
POST: {command: 'submit', deprecated: true},
},
'/session/:sessionId/keys': {
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
},
'/session/:sessionId/element/:elementId/equals/:otherId': {
GET: {command: 'equalsElement', deprecated: true},
},
'/session/:sessionId/element/:elementId/location': {
GET: {command: 'getLocation', deprecated: true},
},
'/session/:sessionId/element/:elementId/location_in_view': {
GET: {command: 'getLocationInView', deprecated: true},
},
'/session/:sessionId/element/:elementId/size': {
GET: {command: 'getSize', deprecated: true},
},
'/session/:sessionId/moveto': {
POST: {
command: 'moveTo',
@@ -355,11 +625,7 @@ export const METHOD_MAP = /** @type {const} */ ({
POST: {deprecated: true},
},
'/session/:sessionId/touch/doubleclick': {
POST: {},
},
'/session/:sessionId/actions': {
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
DELETE: {command: 'releaseActions'},
POST: {deprecated: true},
},
'/session/:sessionId/touch/longclick': {
POST: {
@@ -377,10 +643,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/location': {
GET: {command: 'getGeoLocation'},
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
},
'/session/:sessionId/local_storage': {
GET: {deprecated: true},
POST: {deprecated: true},
@@ -405,46 +667,32 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/session_storage/size': {
GET: {deprecated: true},
},
// Selenium 4 clients
'/session/:sessionId/se/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
// Selenium 4 clients
'/session/:sessionId/se/log/types': {
GET: {command: 'getLogTypes'},
},
// mjsonwire, appium clients
'/session/:sessionId/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}},
},
// mjsonwire, appium clients
'/session/:sessionId/log/types': {
GET: {command: 'getLogTypes'},
},
'/session/:sessionId/application_cache/status': {
GET: {},
GET: {deprecated: true},
},
//
// mjsonwire
//
'/session/:sessionId/context': {
GET: {command: 'getCurrentContext'},
POST: {command: 'setContext', payloadParams: {required: ['name']}},
'/session/:sessionId/alert_text': {
GET: {command: 'getAlertText', deprecated: true},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
deprecated: true
},
},
'/session/:sessionId/contexts': {
GET: {command: 'getContexts'},
'/session/:sessionId/accept_alert': {
POST: {command: 'postAcceptAlert', deprecated: true},
},
'/session/:sessionId/dismiss_alert': {
POST: {command: 'postDismissAlert', deprecated: true},
},
// Pre-W3C endpoint for element screenshot
'/session/:sessionId/screenshot/:elementId': {
GET: {command: 'getElementScreenshot', deprecated: true},
},
// #endregion
// #region Appium specific
'/session/:sessionId/element/:elementId/pageIndex': {
GET: {command: 'getPageIndex', deprecated: true},
},
'/session/:sessionId/network_connection': {
GET: {command: 'getNetworkConnection'},
POST: {
command: 'setNetworkConnection',
payloadParams: {unwrap: 'parameters', required: ['type']},
},
},
'/session/:sessionId/touch/perform': {
POST: {
command: 'performTouch',
@@ -459,19 +707,9 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
payloadParams: {required: ['status', 'value']},
},
},
'/session/:sessionId/appium/device/shake': {
POST: {command: 'mobileShake', deprecated: true},
},
'/session/:sessionId/appium/device/system_time': {
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
},
'/session/:sessionId/appium/device/lock': {
POST: {command: 'lock', payloadParams: {optional: ['seconds']}, deprecated: true},
},
@@ -575,85 +813,6 @@ export const METHOD_MAP = /** @type {const} */ ({
'/session/:sessionId/appium/device/current_package': {
GET: {command: 'getCurrentPackage', deprecated: true},
},
//region Applications Management
'/session/:sessionId/appium/device/install_app': {
POST: {
command: 'installApp',
payloadParams: {
required: ['appPath'],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/activate_app': {
POST: {
command: 'activateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/remove_app': {
POST: {
command: 'removeApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/terminate_app': {
POST: {
command: 'terminateApp',
payloadParams: {
required: [['appId'], ['bundleId']],
optional: ['options'],
},
},
},
'/session/:sessionId/appium/device/app_installed': {
POST: {
command: 'isAppInstalled',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
},
'/session/:sessionId/appium/device/app_state': {
GET: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
},
POST: {
command: 'queryAppState',
payloadParams: {
required: [['appId'], ['bundleId']],
},
deprecated: true,
},
},
//endregion
'/session/:sessionId/appium/device/hide_keyboard': {
POST: {
command: 'hideKeyboard',
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
},
},
'/session/:sessionId/appium/device/is_keyboard_shown': {
GET: {command: 'isKeyboardShown'},
},
'/session/:sessionId/appium/device/push_file': {
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
},
'/session/:sessionId/appium/device/pull_file': {
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/pull_folder': {
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
},
'/session/:sessionId/appium/device/toggle_airplane_mode': {
POST: {command: 'toggleFlightMode', deprecated: true},
},
@@ -743,10 +902,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/appium/settings': {
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
GET: {command: 'getSettings'},
},
'/session/:sessionId/appium/receive_async_response': {
POST: {
command: 'receiveAsyncResponse',
@@ -754,94 +909,6 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
'/session/:sessionId/appium/events': {
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/appium/log_event': {
POST: {
command: 'logCustomEvent',
payloadParams: {required: ['vendor', 'event']},
},
},
/*
* The W3C spec has some changes to the wire protocol.
* https://w3c.github.io/webdriver/webdriver-spec.html
* Begin to add those changes here, keeping the old version
* since clients still implement them.
*/
// MJSONWP
'/session/:sessionId/alert_text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
// MJSONWP
'/session/:sessionId/accept_alert': {
POST: {command: 'postAcceptAlert'},
},
// MJSONWP
'/session/:sessionId/dismiss_alert': {
POST: {command: 'postDismissAlert'},
},
// https://w3c.github.io/webdriver/webdriver-spec.html#user-prompts
'/session/:sessionId/alert/text': {
GET: {command: 'getAlertText'},
POST: {
command: 'setAlertText',
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
},
},
'/session/:sessionId/alert/accept': {
POST: {command: 'postAcceptAlert'},
},
'/session/:sessionId/alert/dismiss': {
POST: {command: 'postDismissAlert'},
},
// https://w3c.github.io/webdriver/webdriver-spec.html#get-element-rect
'/session/:sessionId/element/:elementId/rect': {
GET: {command: 'getElementRect'},
},
'/session/:sessionId/execute/sync': {
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
},
'/session/:sessionId/execute/async': {
POST: {
command: 'executeAsync',
payloadParams: {required: ['script', 'args']},
},
},
// Pre-W3C endpoint for element screenshot
'/session/:sessionId/screenshot/:elementId': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/element/:elementId/screenshot': {
GET: {command: 'getElementScreenshot'},
},
'/session/:sessionId/window/rect': {
GET: {command: 'getWindowRect'},
POST: {
command: 'setWindowRect',
payloadParams: {optional: ['x', 'y', 'width', 'height']},
},
},
'/session/:sessionId/window/maximize': {
POST: {command: 'maximizeWindow'},
},
'/session/:sessionId/window/minimize': {
POST: {command: 'minimizeWindow'},
},
'/session/:sessionId/window/fullscreen': {
POST: {command: 'fullScreenWindow'},
},
'/session/:sessionId/window/new': {
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
},
'/session/:sessionId/element/:elementId/property/:name': {
GET: {command: 'getProperty'},
},
'/session/:sessionId/appium/device/set_clipboard': {
POST: {
command: 'setClipboard',
@@ -861,61 +928,15 @@ export const METHOD_MAP = /** @type {const} */ ({
deprecated: true,
},
},
// chromium devtools
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
'/session/:sessionId/:vendor/cdp/execute': {
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
// #endregion
// #region JSONWP
'/session/:sessionId/log': {
POST: {command: 'getLog', payloadParams: {required: ['type']}, deprecated: true},
},
//region Webauthn
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
'/session/:sessionId/webauthn/authenticator': {
POST: {
command: 'addVirtualAuthenticator',
payloadParams: {
required: ['protocol', 'transport'],
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
},
},
'/session/:sessionId/log/types': {
GET: {command: 'getLogTypes', deprecated: true},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
DELETE: {
command: 'removeVirtualAuthenticator',
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
POST: {
command: 'addAuthCredential',
payloadParams: {
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
optional: ['userHandle', 'signCount'],
},
},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
GET: {command: 'getAuthCredential'},
DELETE: {command: 'removeAllAuthCredentials'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
DELETE: {command: 'removeAuthCredential'},
},
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
POST: {
command: 'setUserAuthVerified',
payloadParams: {
required: ['isUserVerified'],
},
},
},
//endregion
// #endregion
});
// driver command names
@@ -926,7 +947,7 @@ export const ALL_COMMANDS = _.flatMap(_.values(METHOD_MAP).map(_.values))
/**
*
* @param {string} endpoint
* @param {import('@appium/types').HTTPMethod} method
* @param {import('@appium/types').HTTPMethod} [method]
* @param {string} [basePath=DEFAULT_BASE_PATH]
* @returns {string|undefined}
*/
@@ -944,23 +965,47 @@ export function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PAT
throw new Error(`'${endpoint}' cannot be translated to a command name: ${err.message}`);
}
const normalizedMethod = _.toUpper(method);
const cacheKey = toCommandNameCacheKey(normalizedPathname, normalizedMethod);
if (COMMAND_NAMES_CACHE.has(cacheKey)) {
return COMMAND_NAMES_CACHE.get(cacheKey) || undefined;
}
/** @type {string[]} */
const possiblePathnames = [];
if (!normalizedPathname.startsWith('/session/')) {
possiblePathnames.push(`/session/any-session-id${normalizedPathname}`);
}
possiblePathnames.push(normalizedPathname);
const normalizedMethod = _.toUpper(method);
for (const [routePath, routeSpec] of _.toPairs(METHOD_MAP)) {
const routeMatcher = match(routePath);
if (possiblePathnames.some((pp) => routeMatcher(pp))) {
const commandName = routeSpec?.[normalizedMethod]?.command;
const commandForAnyMethod = () => _.first(
(_.keys(routeSpec) ?? []).map((key) => routeSpec[key]?.command)
);
const commandName = normalizedMethod
? routeSpec?.[normalizedMethod]?.command
: commandForAnyMethod();
if (commandName) {
COMMAND_NAMES_CACHE.set(cacheKey, commandName);
return commandName;
}
}
}
// storing an empty string means we did not find any match for this set of arguments
// and we want to cache this result
COMMAND_NAMES_CACHE.set(cacheKey, '');
}
/**
*
* @param {string} endpoint
* @param {string} [method]
* @returns {string}
*/
function toCommandNameCacheKey(endpoint, method) {
return `${endpoint}:${method ?? ''}`;
}
// driver commands that do not require a session to already exist
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions'];
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions', 'getAppiumSessions'];
+6 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "9.15.0",
"version": "9.16.3",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -44,21 +44,16 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^6.0.3",
"@appium/types": "^0.24.0",
"@appium/support": "^6.0.7",
"@appium/types": "^0.25.2",
"@colors/colors": "1.6.0",
"@types/async-lock": "1.4.2",
"@types/bluebird": "3.5.42",
"@types/express": "5.0.0",
"@types/lodash": "4.17.14",
"@types/method-override": "3.0.0",
"@types/serve-favicon": "2.5.7",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.7.9",
"axios": "1.8.2",
"bluebird": "3.7.2",
"body-parser": "1.20.3",
"express": "4.21.2",
"fastest-levenshtein": "1.0.16",
"http-status-codes": "2.3.0",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
@@ -67,7 +62,7 @@
"path-to-regexp": "8.2.0",
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"type-fest": "4.31.0",
"type-fest": "4.37.0",
"validate.js": "0.13.1"
},
"optionalDependencies": {
@@ -1,13 +1,63 @@
import {BaseDriver} from '../../../lib';
import {driverE2ETestSuite} from '@appium/driver-test-support';
import { BaseDriver, server, routeConfiguringFunction } from '../../../lib';
import { FakeDriver } from '../protocol/fake-driver';
import axios from 'axios';
import {TEST_HOST, getTestPort, driverE2ETestSuite} from '@appium/driver-test-support';
const DEFAULT_CAPS = {
platformName: 'iOS',
'appium:deviceName': 'Delorean',
platformName: 'fake',
'appium:automationNAme': 'fake',
};
driverE2ETestSuite(BaseDriver, DEFAULT_CAPS);
driverE2ETestSuite(BaseDriver, {
platformName: 'iOS',
'appium:deviceName': 'Delorean',
});
/**
* @typedef {import('@appium/driver-test-support').SessionHelpers} SessionHelpers
*/
describe('BaseDriver', function () {
let port;
let baseUrl;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
port = await getTestPort();
baseUrl = `http://${TEST_HOST}:${port}`;
});
describe('get appium capabilities', function () {
let driver;
let sessionId = 'foo';
let mjsonwpServer;
before(async function () {
driver = new FakeDriver();
driver.sessionId = sessionId;
mjsonwpServer = await server({
routeConfiguringFunction: routeConfiguringFunction(driver),
port,
});
});
after(async function () {
await mjsonwpServer.close();
});
it('should return capabilities', async function () {
const capabilities = DEFAULT_CAPS;
driver.caps = capabilities;
const {data} = await axios({
url: `${baseUrl}/session/${sessionId}/appium/capabilities`,
method: 'GET',
});
data.should.eql({
value: {
capabilities: DEFAULT_CAPS
},
sessionId
});
});
});
});
@@ -153,4 +153,7 @@ describe('parseCapsArray', function () {
it('should return an array without change', function () {
parseCapsArray(['a', 'b']).should.eql(['a', 'b']);
});
it('should fail if an invalid JSON array is provided', function () {
(() => parseCapsArray(`['*']`)).should.throw();
});
});
@@ -10,6 +10,9 @@ function buildReqRes(url, method, body) {
res.set = (k, v) => {
res[k] = v;
};
res.setHeader = (k, v) => {
res.headers[k] = v;
};
res.status = (code) => {
res.sentCode = code;
return res;
@@ -61,52 +64,86 @@ describe('proxy', function () {
});
describe('getUrlForProxy', function () {
it('should modify session id, host, and port', function () {
let j = mockProxy({sessionId: '123'});
j.getUrlForProxy('http://host.com:1234/session/456/element/200/value').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
mockProxy({sessionId: '123'})
.getUrlForProxy('http://host.com:1234/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
});
it('should prepend scheme, host and port if not provided', function () {
let j = mockProxy({sessionId: '123'});
j.getUrlForProxy('/session/456/element/200/value').should.eql(
j.getUrlForProxy('/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
j.getUrlForProxy('/session/456/appium/settings', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/appium/settings`
);
});
it('should prepend scheme, host, port and session if not provided', function () {
mockProxy({sessionId: '123'})
.getUrlForProxy('/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
});
it('should keep query parameters', function () {
mockProxy({sessionId: '123'})
.getUrlForProxy('/element/200/value?foo=1&bar=2', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value?foo=1&bar=2`
);
});
it('should fix legacy proxy urls if reqBasePath is unset', function () {
const j = mockProxy({sessionId: '123', reqBasePath: ''});
j.getUrlForProxy('/wd/hub/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
j.getUrlForProxy('/yolo/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
});
it('should respect nonstandard incoming request base path', function () {
let j = mockProxy({sessionId: '123', reqBasePath: ''});
j.getUrlForProxy('/session/456/element/200/value').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
mockProxy({sessionId: '123', reqBasePath: ''})
.getUrlForProxy('/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
j = mockProxy({sessionId: '123', reqBasePath: '/my/base/path'});
j.getUrlForProxy('/my/base/path/session/456/element/200/value').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
mockProxy({sessionId: '123', reqBasePath: '/my/base/path'})
.getUrlForProxy('/my/base/path/session/456/element/200/value', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session/123/element/200/value`
);
mockProxy({sessionId: '123', reqBasePath: '/wd/hub'})
.getUrlForProxy('/wd/hub/session/456', 'GET').should.eql(
`http://${TEST_HOST}:${port}/session/123`
);
mockProxy({reqBasePath: '/my/base/path'})
.getUrlForProxy('/my/base/path/session', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session`
);
});
it('should work with urls which do not have session ids', function () {
let j = mockProxy({sessionId: '123'});
j.getUrlForProxy('http://host.com:1234/session').should.eql(
j.getUrlForProxy('http://host.com:1234/session', 'POST').should.eql(
`http://${TEST_HOST}:${port}/session`
);
let newUrl = j.getUrlForProxy('/session');
newUrl.should.eql(`http://${TEST_HOST}:${port}/session`);
j.getUrlForProxy('/session', 'POST')
.should.eql(`http://${TEST_HOST}:${port}/session`);
j.getUrlForProxy('/appium/sessions', 'GET')
.should.eql(`http://${TEST_HOST}:${port}/appium/sessions`);
});
it('should throw an error if url requires a sessionId but its null', function () {
let j = mockProxy();
let e;
try {
j.getUrlForProxy('/session/456/element/200/value');
j.getUrlForProxy('/session/456/element/200/value', 'POST');
} catch (err) {
e = err;
}
should.exist(e);
e.message.should.contain('without session id');
e.message.should.contain('not set');
});
it('should not throw an error if url does not require a session id and its null', function () {
let j = mockProxy();
let newUrl = j.getUrlForProxy('/status');
let newUrl = mockProxy().getUrlForProxy('/status', 'GET');
should.exist(newUrl);
});
});
@@ -37,7 +37,7 @@ describe('JWProxy', function () {
it('should translate host and port', function () {
let incomingUrl = PROXY_STATUS_URL;
let j = createJWProxy();
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
proxyUrl.should.equal(testStatusURL);
});
it('should translate the scheme', function () {
@@ -49,21 +49,21 @@ describe('JWProxy', function () {
it('should translate the base', function () {
let incomingUrl = PROXY_STATUS_URL;
let j = createJWProxy({base: ''});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
proxyUrl.should.equal(testStatusURL);
});
it('should translate the session id', function () {
let incomingUrl = createProxyURL('foobar', 'element');
let j = createJWProxy({sessionId: 'barbaz'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
});
it('should error when translating session commands without session id', function () {
let incomingUrl = createProxyURL('foobar', 'element');
let j = createJWProxy();
(() => {
j.getUrlForProxy(incomingUrl);
}).should.throw('session id');
j.getUrlForProxy(incomingUrl, 'POST');
}).should.throw('not set');
});
});
@@ -71,7 +71,7 @@ describe('JWProxy', function () {
it('should proxy /status', function () {
let incomingUrl = '/status';
let j = createJWProxy();
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
proxyUrl.should.equal(testStatusURL);
});
it('should proxy /session', function () {
@@ -83,64 +83,64 @@ describe('JWProxy', function () {
it('should proxy /sessions', function () {
let incomingUrl = '/sessions';
let j = createJWProxy();
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
proxyUrl.should.equal(createTestURL('', 'sessions'));
});
it('should proxy session commands based off /session', function () {
let incomingUrl = '/session/foobar/element';
let j = createJWProxy({sessionId: 'barbaz'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
});
it('should error session commands based off /session without session id', function () {
let incomingUrl = '/session/foobar/element';
let j = createJWProxy();
(() => {
j.getUrlForProxy(incomingUrl);
}).should.throw('session id');
j.getUrlForProxy(incomingUrl, 'POST');
}).should.throw('not set');
});
it('should proxy session commands based off ', function () {
let incomingUrl = '/session/3d001db2-7987-42a7-975d-8d5d5304083f/timeouts/implicit_wait';
let j = createJWProxy({sessionId: '123'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('123', 'timeouts/implicit_wait'));
});
it('should proxy session commands based off /session as ""', function () {
let incomingUrl = '';
let j = createJWProxy();
(() => {
j.getUrlForProxy(incomingUrl);
}).should.throw('session id');
j.getUrlForProxy(incomingUrl, 'GET');
}).should.throw('not set');
j = createJWProxy({sessionId: '123'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
proxyUrl.should.equal(createTestSessionURL('123'));
});
it('should proxy session commands without /session', function () {
let incomingUrl = '/element';
let j = createJWProxy({sessionId: 'barbaz'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
});
it(`should proxy session commands when '/session' is in the url`, function () {
let incomingUrl =
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/cookie/session-something-or-other';
let j = createJWProxy({sessionId: 'barbaz'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('barbaz', 'cookie/session-something-or-other'));
});
it(`should proxy session commands when '/session' is in the url and not base on the original url`, function () {
let incomingUrl =
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/cookie/session-something-or-other';
let j = createJWProxy({sessionId: 'barbaz'});
let proxyUrl = j.getUrlForProxy(incomingUrl);
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
proxyUrl.should.equal(createTestURL('barbaz', 'cookie/session-something-or-other'));
});
it('should error session commands without /session without session id', function () {
let incomingUrl = '/element';
let j = createJWProxy();
(() => {
j.getUrlForProxy(incomingUrl);
}).should.throw('session id');
j.getUrlForProxy(incomingUrl, 'POST');
}).should.throw('not set');
});
});
});
@@ -41,7 +41,7 @@ describe('Protocol', function () {
}
let hash = shasum.digest('hex').substring(0, 8);
// Modify the hash whenever the protocol has intentionally been modified.
hash.should.equal('6e93ad90');
hash.should.equal('00fcb083');
});
});
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.4](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.3...@appium/base-plugin@2.3.4) (2025-03-11)
**Note:** Version bump only for package @appium/base-plugin
## [2.3.3](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.2...@appium/base-plugin@2.3.3) (2025-02-20)
**Note:** Version bump only for package @appium/base-plugin
## [2.3.2](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.1...@appium/base-plugin@2.3.2) (2025-02-20)
**Note:** Version bump only for package @appium/base-plugin
## [2.3.1](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.0...@appium/base-plugin@2.3.1) (2025-02-19)
**Note:** Version bump only for package @appium/base-plugin
## [2.3.0](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.52...@appium/base-plugin@2.3.0) (2025-01-08)
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "2.3.0",
"version": "2.3.4",
"description": "The base plugin used to create Appium 2.0 plugins",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.15.0",
"@appium/support": "^6.0.3"
"@appium/base-driver": "^9.16.3",
"@appium/support": "^6.0.7"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.33](https://github.com/appium/appium/compare/@appium/docutils@1.0.32...@appium/docutils@1.0.33) (2025-03-11)
**Note:** Version bump only for package @appium/docutils
## [1.0.32](https://github.com/appium/appium/compare/@appium/docutils@1.0.31...@appium/docutils@1.0.32) (2025-02-20)
**Note:** Version bump only for package @appium/docutils
## [1.0.31](https://github.com/appium/appium/compare/@appium/docutils@1.0.30...@appium/docutils@1.0.31) (2025-02-20)
**Note:** Version bump only for package @appium/docutils
## [1.0.30](https://github.com/appium/appium/compare/@appium/docutils@1.0.29...@appium/docutils@1.0.30) (2025-02-19)
### Bug Fixes
* **docutils:** update dependency consola to v3.4.0 ([#20908](https://github.com/appium/appium/issues/20908)) ([8fb0da3](https://github.com/appium/appium/commit/8fb0da34bb58c44c30ac8900a62510f1fcb663bc))
* **docutils:** update dependency typescript to v5.7.3 ([#20893](https://github.com/appium/appium/issues/20893)) ([6706a00](https://github.com/appium/appium/commit/6706a0037eda5f7eb9eb2a4b0fb49888618c12c2))
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [1.0.29](https://github.com/appium/appium/compare/@appium/docutils@1.0.28...@appium/docutils@1.0.29) (2025-01-08)
**Note:** Version bump only for package @appium/docutils
+8 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "1.0.29",
"version": "1.0.33",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -48,23 +48,22 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "^6.0.3",
"@appium/tsconfig": "^0.3.3",
"@appium/support": "^6.0.7",
"@appium/tsconfig": "^0.3.5",
"@sliphua/lilconfig-ts-loader": "3.2.2",
"@types/which": "3.0.4",
"chalk": "4.1.2",
"consola": "3.3.3",
"consola": "3.4.0",
"diff": "7.0.0",
"json5": "2.2.3",
"lilconfig": "3.1.3",
"lodash": "4.17.21",
"pkg-dir": "5.0.0",
"read-pkg": "5.2.0",
"semver": "7.6.3",
"semver": "7.7.1",
"source-map-support": "0.5.21",
"teen_process": "2.2.3",
"type-fest": "4.31.0",
"typescript": "5.7.2",
"teen_process": "2.3.1",
"type-fest": "4.37.0",
"typescript": "5.8.2",
"yaml": "2.7.0",
"yargs": "17.7.2",
"yargs-parser": "21.1.1"
+2 -2
View File
@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-git-revision-date-localized-plugin==1.3.0
mkdocs-material==9.5.49
mkdocs-git-revision-date-localized-plugin==1.4.4
mkdocs-material==9.6.7
mkdocs-redirects==1.2.2
mike==2.1.3
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.7.7](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.6...@appium/driver-test-support@0.7.7) (2025-03-11)
**Note:** Version bump only for package @appium/driver-test-support
## [0.7.6](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.5...@appium/driver-test-support@0.7.6) (2025-02-20)
**Note:** Version bump only for package @appium/driver-test-support
## [0.7.5](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.4...@appium/driver-test-support@0.7.5) (2025-02-19)
### Bug Fixes
* **driver-test-support:** update definitelytyped ([#20942](https://github.com/appium/appium/issues/20942)) ([f7c63ee](https://github.com/appium/appium/commit/f7c63ee5d87be21eba577a4cfd0b0a08a050afeb))
* **driver-test-support:** update dependency chai to v5.2.0 ([#20998](https://github.com/appium/appium/issues/20998)) ([ca456e5](https://github.com/appium/appium/commit/ca456e52365a9e621b4473263cc78bb2a3a855e2))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [0.7.4](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.3...@appium/driver-test-support@0.7.4) (2025-01-08)
**Note:** Version bump only for package @appium/driver-test-support
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "0.7.4",
"version": "0.7.7",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -41,17 +41,16 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"@appium/types": "^0.24.0",
"@types/lodash": "4.17.14",
"axios": "1.7.9",
"@appium/types": "^0.25.2",
"axios": "1.8.2",
"bluebird": "3.7.2",
"chai": "5.1.2",
"chai": "5.2.0",
"chai-as-promised": "8.0.1",
"get-port": "5.1.1",
"lodash": "4.17.21",
"sinon": "19.0.2",
"source-map-support": "0.5.21",
"type-fest": "4.31.0"
"type-fest": "4.37.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43",
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.2...@appium/eslint-config-appium-ts@1.0.3) (2025-02-19)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
## [1.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.1...@appium/eslint-config-appium-ts@1.0.2) (2025-01-08)
@@ -1,7 +1,7 @@
{
"name": "@appium/eslint-config-appium-ts",
"type": "module",
"version": "1.0.2",
"version": "1.0.3",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
"eslint",
@@ -32,7 +32,7 @@
"@eslint/compat": "^1.1.0",
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^9.0.0 || ^10.0.0",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-mocha": "^10.4.0",
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.0.3](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.2...@appium/execute-driver-plugin@4.0.3) (2025-03-11)
**Note:** Version bump only for package @appium/execute-driver-plugin
## [4.0.2](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.1...@appium/execute-driver-plugin@4.0.2) (2025-02-19)
### Bug Fixes
* **execute-driver-plugin:** update dependency webdriverio to v9.5.3 ([#20895](https://github.com/appium/appium/issues/20895)) ([8741474](https://github.com/appium/appium/commit/874147449a26bbec03b08e7e95fb9e4c6b5881af))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.4 ([#20898](https://github.com/appium/appium/issues/20898)) ([7f91d5c](https://github.com/appium/appium/commit/7f91d5cba850100124088d774ba8704e3765863f))
* **execute-driver-plugin:** update dependency webdriverio to v9.5.7 ([#20903](https://github.com/appium/appium/issues/20903)) ([c60fdc8](https://github.com/appium/appium/commit/c60fdc8d847b4aeed2d8b8307180194ee32e5ca9))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.0 ([#20930](https://github.com/appium/appium/issues/20930)) ([5780982](https://github.com/appium/appium/commit/57809826a410b71dca155f59e081f77dcaf671ed))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.1 ([#20934](https://github.com/appium/appium/issues/20934)) ([50fa5d6](https://github.com/appium/appium/commit/50fa5d670bd967200a78e5b4b8a54d5c9b85901b))
* **execute-driver-plugin:** update dependency webdriverio to v9.7.2 ([#20944](https://github.com/appium/appium/issues/20944)) ([f5e0819](https://github.com/appium/appium/commit/f5e0819f505b55421b7d260f69a41ae980390a91))
* **execute-driver-plugin:** update dependency webdriverio to v9.8.0 ([#20964](https://github.com/appium/appium/issues/20964)) ([ce32226](https://github.com/appium/appium/commit/ce32226cb57fb3f07bf4862dbbfcba227ff1230c))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.0 ([#20994](https://github.com/appium/appium/issues/20994)) ([b3c877f](https://github.com/appium/appium/commit/b3c877f66ddf3ccf7a25ca1f7a3952c660bd7dda))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.1 ([#20996](https://github.com/appium/appium/issues/20996)) ([c6a080f](https://github.com/appium/appium/commit/c6a080f7ade03a49a274ce6fbc84629406fdc2a7))
* **execute-driver-plugin:** update dependency webdriverio to v9.9.3 ([#21005](https://github.com/appium/appium/issues/21005)) ([686a630](https://github.com/appium/appium/commit/686a630fbdf525db16b42fdfc1bbdaf6426b41d0))
## [4.0.1](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.0...@appium/execute-driver-plugin@4.0.1) (2025-01-06)
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/execute-driver-plugin",
"version": "4.0.1",
"version": "4.0.3",
"description": "Plugin for batching and executing driver commands with Appiums",
"keywords": [
"automation",
@@ -38,11 +38,10 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@types/bluebird": "3.5.42",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"source-map-support": "0.5.21",
"webdriverio": "9.5.1"
"webdriverio": "9.10.1"
},
"peerDependencies": {
"appium": "^2.0.0-beta.35"
+17
View File
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.2](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.1...@appium/fake-driver@5.7.2) (2025-03-11)
**Note:** Version bump only for package @appium/fake-driver
## [5.7.1](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.0...@appium/fake-driver@5.7.1) (2025-02-19)
### Bug Fixes
* **universal-xml-plugin:** update dependency @xmldom/xmldom to v0.9.7 ([#20918](https://github.com/appium/appium/issues/20918)) ([7e1f1e5](https://github.com/appium/appium/commit/7e1f1e5aad19d709a48cd4d06196fa4db792400f))
## [5.7.0](https://github.com/appium/appium/compare/@appium/fake-driver@5.6.0...@appium/fake-driver@5.7.0) (2025-01-08)
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-driver",
"version": "5.7.0",
"version": "5.7.2",
"description": "Mock driver used internally by Appium for testing. Ignore",
"keywords": [
"automation",
@@ -42,8 +42,7 @@
"test:unit": "mocha --timeout 10s --slow 5s \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@types/bluebird": "3.5.42",
"@xmldom/xmldom": "0.9.6",
"@xmldom/xmldom": "0.9.8",
"asyncbox": "3.0.0",
"bluebird": "3.7.2",
"lodash": "4.17.21",
@@ -51,7 +50,7 @@
"xpath": "0.0.34"
},
"peerDependencies": {
"appium": "^2.4.1"
"appium": "^2.4.1 || ^3.0.0-beta.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.2.4](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.3...@appium/fake-plugin@3.2.4) (2025-03-11)
**Note:** Version bump only for package @appium/fake-plugin
## [3.2.3](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.2...@appium/fake-plugin@3.2.3) (2025-02-20)
**Note:** Version bump only for package @appium/fake-plugin
## [3.2.2](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.1...@appium/fake-plugin@3.2.2) (2025-02-20)
**Note:** Version bump only for package @appium/fake-plugin
## [3.2.1](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.0...@appium/fake-plugin@3.2.1) (2025-02-19)
**Note:** Version bump only for package @appium/fake-plugin
## [3.2.0](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.56...@appium/fake-plugin@3.2.0) (2025-01-08)
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-plugin",
"version": "3.2.0",
"version": "3.2.4",
"description": "A fake Appium 2.0 plugin",
"keywords": [
"automation",
@@ -39,9 +39,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-plugin": "^2.3.0",
"@appium/support": "^6.0.3",
"@types/bluebird": "3.5.42",
"@appium/base-plugin": "^2.3.4",
"@appium/support": "^6.0.7",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"source-map-support": "0.5.21"
+33
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.1.0](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.30...@appium/images-plugin@3.1.0) (2025-03-11)
### Features
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
## [3.0.30](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.29...@appium/images-plugin@3.0.30) (2025-02-20)
**Note:** Version bump only for package @appium/images-plugin
## [3.0.29](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.28...@appium/images-plugin@3.0.29) (2025-02-20)
**Note:** Version bump only for package @appium/images-plugin
## [3.0.28](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.27...@appium/images-plugin@3.0.28) (2025-02-19)
**Note:** Version bump only for package @appium/images-plugin
## [3.0.27](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.26...@appium/images-plugin@3.0.27) (2025-01-08)
**Note:** Version bump only for package @appium/images-plugin
+43 -2
View File
@@ -1,7 +1,6 @@
import _ from 'lodash';
import {errors} from 'appium/driver';
import {util} from '@appium/support';
import {BasePlugin} from 'appium/plugin';
import {compareImages} from './compare';
import ImageElementFinder from './finder';
@@ -83,6 +82,48 @@ export default class ImageElementPlugin extends BasePlugin {
// otherwise just do the normal thing
return await next();
}
async performActions(next, driver, ...args) {
// Replace with coordinates when ActionSequence includes image elements.
const [actionSequences] = /** @type {[import('@appium/types').ActionSequence[]]} */ (args);
for (const actionSequence of actionSequences) {
for (const action of actionSequence.actions) {
// The actions that can have an Element as the origin are "pointerMove" and "scroll".
if (
!_.isPlainObject(
/** @type {{origin?: "viewport" | "pointer" | import('@appium/types').Element}} */ (
action
).origin,
)
) {
continue;
}
const actionWithEl =
/** @type {import('@appium/types').PointerMoveAction | import('@appium/types').ScrollAction} */ (
action
);
const elId = util.unwrapElement(/** @type {import('@appium/types').Element} */ (actionWithEl.origin));
if (!_.startsWith(elId, IMAGE_ELEMENT_PREFIX)) {
continue;
}
const imgEl = this.finder.getImageElement(elId);
if (!imgEl) {
throw new errors.NoSuchElementError();
}
// Add the element's center coordinates to the offset value.
actionWithEl.x += imgEl.center.x;
actionWithEl.y += imgEl.center.y;
// Set the origin to the viewport so that the external driver can process it using coordinates.
delete actionWithEl.origin;
}
}
return await next();
}
}
export {ImageElementPlugin, getImgElFromArgs, IMAGE_STRATEGY};
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/images-plugin",
"version": "3.0.27",
"version": "3.1.0",
"description": "Plugin for working with images and image elements in Appium",
"keywords": [
"automation",
@@ -34,13 +34,13 @@
],
"scripts": {
"test": "npm run test:unit",
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.js\"",
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.*js\"",
"test:smoke": "node ./index.js",
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
"test:unit": "mocha \"./test/unit/**/*.spec.*js\""
},
"dependencies": {
"@appium/opencv": "^3.0.7",
"@appium/support": "^6.0.3",
"@appium/opencv": "^3.0.8",
"@appium/support": "^6.0.7",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
"sharp": "0.33.5",
@@ -2,7 +2,7 @@ import _ from 'lodash';
import path from 'path';
import {remote as wdio} from 'webdriverio';
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE} from '../../lib/constants';
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures';
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures/index.cjs';
import {pluginE2EHarness} from '@appium/plugin-test-support';
import {tempDir, fs} from '@appium/support';
import sharp from 'sharp';
@@ -10,7 +10,7 @@ import sharp from 'sharp';
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
const TEST_HOST = 'localhost';
const TEST_HOST = '127.0.0.1';
const TEST_PORT = 4723;
const TEST_FAKE_APP = path.join(
APPIUM_HOME,
@@ -89,6 +89,19 @@ describe('ImageElementPlugin', function () {
width.should.eql(80);
height.should.eql(91);
await imageEl.click();
const actionSequence = {
type: 'pointer',
id: 'mouse',
parameters: {pointerType: 'touch'},
actions: [
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
{type: 'pointerDown', button: 0},
{type: 'pause', duration: 125},
{type: 'pointerUp', button: 0},
],
};
await driver.performActions([actionSequence]);
});
it('should find subelements', async function () {
@@ -6,7 +6,7 @@ import {IMAGE_STRATEGY} from '../../lib/constants';
import ImageElementFinder from '../../lib/finder';
import {ImageElement} from '../../lib/image-element';
import sinon from 'sinon';
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures';
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures/index.cjs';
import sharp from 'sharp';
const compareModule = require('../../lib/compare');
@@ -1,3 +1,4 @@
import _ from 'lodash';
import {ImageElementPlugin} from '../../lib/plugin';
import {
MATCH_FEATURES_MODE,
@@ -6,7 +7,7 @@ import {
IMAGE_STRATEGY,
} from '../../lib/constants';
import {BaseDriver} from 'appium/driver';
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures';
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures/index.cjs';
import {util} from '@appium/support';
describe('ImageElementPlugin#handle', function () {
@@ -177,4 +178,91 @@ describe('ImageElementPlugin#handle', function () {
.should.eventually.be.rejectedWith(/not yet/i);
});
});
describe('performActions', function () {
let imageEl;
let nativeEl;
before(async function () {
imageEl = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
nativeEl = util.wrapElement('dummy-native-element-id');
});
it('should replace with coords of the image elements in pointerMove, scroll actions', async function () {
const actionSequences = [
{
type: 'pointer',
id: 'mouse',
parameters: {pointerType: 'touch'},
actions: [
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
{type: 'pointerMove', x: 15, y: 25, duration: 0, origin: imageEl},
],
},
{
type: 'wheel',
id: 'wheel',
actions: [
{type: 'scroll', x: 1, y: 0, deltaX: 1, deltaY: 2, origin: imageEl},
],
},
];
await p.performActions(next, driver, actionSequences);
actionSequences.should.eql([
{
type: 'pointer',
id: 'mouse',
parameters: {pointerType: 'touch'},
actions: [
{type: 'pointerMove', x: 24, y: 40, duration: 0},
{type: 'pointerMove', x: 39, y: 65, duration: 0},
],
},
{
type: 'wheel',
id: 'wheel',
actions: [
{type: 'scroll', x: 25, y: 40, deltaX: 1, deltaY: 2},
],
},
]);
});
it('should not be modified except pointerMove and scroll actions includes image element as origin', async function () {
const actionSequences = [
{
type: 'pointer',
id: 'mouse',
parameters: {pointerType: 'touch'},
actions: [
{type: 'pointerMove', x: 1, y: 1, duration: 0},
{type: 'pointerMove', x: 2, y: 2, duration: 10, origin: nativeEl},
{type: 'pointerMove', x: 3, y: 3, duration: 20, origin: 'viewport'},
{type: 'pointerMove', x: 4, y: 4, duration: 30, origin: 'pointer'},
{type: 'pointerDown', button: 0},
{type: 'pause', duration: 125},
{type: 'pointerUp', button: 0},
],
},
{
type: 'wheel',
id: 'wheel',
actions: [
{type: 'scroll', x: 1, y: 1, deltaX: 1, deltaY: 2},
{type: 'scroll', x: 2, y: 2, deltaX: 2, deltaY: 3, origin: nativeEl},
{type: 'scroll', x: 3, y: 3, deltaX: 3, deltaY: 4, origin: 'viewport'},
{type: 'scroll', x: 4, y: 4, deltaX: 4, deltaY: 5, origin: 'pointer'},
],
},
{
type: 'key',
id: 'key',
actions: [
{type: 'keyDown', value: 'a'},
{type: 'keyUp', value: 'a'},
],
},
];
const clone = _.cloneDeep(actionSequences);
await p.performActions(next, driver, actionSequences);
actionSequences.should.eql(clone);
});
});
});
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.0.8](https://github.com/appium/appium/compare/@appium/opencv@3.0.7...@appium/opencv@3.0.8) (2025-02-19)
**Note:** Version bump only for package @appium/opencv
## [3.0.7](https://github.com/appium/appium/compare/@appium/opencv@3.0.6...@appium/opencv@3.0.7) (2025-01-02)
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/opencv",
"version": "3.0.7",
"version": "3.0.8",
"description": "OpenCV-related helper library",
"keywords": [
"automation",
@@ -41,7 +41,6 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@types/bluebird": "3.5.42",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"opencv-bindings": "4.5.5",
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.3.52](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.51...@appium/plugin-test-support@0.3.52) (2025-03-11)
**Note:** Version bump only for package @appium/plugin-test-support
## [0.3.51](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.50...@appium/plugin-test-support@0.3.51) (2025-02-20)
**Note:** Version bump only for package @appium/plugin-test-support
## [0.3.50](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.49...@appium/plugin-test-support@0.3.50) (2025-02-19)
**Note:** Version bump only for package @appium/plugin-test-support
## [0.3.49](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.48...@appium/plugin-test-support@0.3.49) (2025-01-08)
**Note:** Version bump only for package @appium/plugin-test-support
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/plugin-test-support",
"version": "0.3.49",
"version": "0.3.52",
"description": "Testing utilities for Appium plugins",
"keywords": [
"automation",
@@ -41,11 +41,11 @@
"test:unit": "exit 0"
},
"dependencies": {
"@appium/types": "^0.24.0",
"@appium/types": "^0.25.2",
"get-port": "5.1.1",
"log-symbols": "4.1.0",
"source-map-support": "0.5.21",
"teen_process": "2.2.3"
"teen_process": "2.3.1"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43",
+17
View File
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.8.1](https://github.com/appium/appium/compare/@appium/schema@0.8.0...@appium/schema@0.8.1) (2025-02-20)
**Note:** Version bump only for package @appium/schema
## [0.8.0](https://github.com/appium/appium/compare/@appium/schema@0.7.1...@appium/schema@0.8.0) (2025-02-19)
### Features
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
## [0.7.1](https://github.com/appium/appium/compare/@appium/schema@0.7.0...@appium/schema@0.7.1) (2025-01-02)
+13 -1
View File
@@ -117,11 +117,23 @@ export const AppiumConfigJsonSchema = /** @type {const} */ ({
appiumCliAliases: ['ka'],
default: 600,
description:
'Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. A value of 0 disables the timeout.',
'Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout ' +
'for all requests. A value of 0 disables the timeout.',
minimum: 0,
title: 'keep-alive-timeout config',
type: 'integer',
},
'request-timeout': {
default: 3600,
description:
'Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. ' +
'A value of 0 disables the timeout. Set it to a non-zero value to protect against ' +
'potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. ' +
'HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.',
minimum: 0,
title: 'request-timeout config',
type: 'integer',
},
'local-timezone': {
default: false,
description: 'Use local timezone for timestamps',
@@ -120,6 +120,13 @@
"title": "keep-alive-timeout config",
"type": "integer"
},
"request-timeout": {
"default": 3600,
"description": "Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. A value of 0 disables the timeout. Set it to a non-zero value to protect against potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.",
"minimum": 0,
"title": "request-timeout config",
"type": "integer"
},
"local-timezone": {
"default": false,
"description": "Use local timezone for timestamps",
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/schema",
"version": "0.7.1",
"version": "0.8.1",
"description": "Appium Configuration Schema",
"keywords": [
"automation",
@@ -19,7 +19,7 @@
"repository": {
"type": "git",
"url": "https://github.com/appium/appium.git",
"directory": "schema"
"directory": "packages/schema"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
@@ -38,7 +38,6 @@
"test": "exit 0"
},
"dependencies": {
"@types/json-schema": "7.0.15",
"json-schema": "0.4.0",
"source-map-support": "0.5.21"
},
+12
View File
@@ -0,0 +1,12 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 0.1.0 (2025-03-11)
### Features
* Add storage plugin ([#21075](https://github.com/appium/appium/issues/21075)) ([ba4aa39](https://github.com/appium/appium/commit/ba4aa394d1b6676cc29644e7faa3b0590552f303))
* **storage-plugin:** Tune the files keeping behaviour ([#21086](https://github.com/appium/appium/issues/21086)) ([15280b8](https://github.com/appium/appium/commit/15280b80d2af6b3bdf6bf2905472b05b7bca1c1d))
+83
View File
@@ -0,0 +1,83 @@
# @appium/storage-plugin
> Appium plugin for server-side file storage
This plugin adds the ability to create a dedicated storage space on the server side,
which can be managed from the client side. This can be useful for files like application packages.
Only one storage may exist per server process, shared by all testing sessions.
> [!WARNING]
> This plugin is designed to be used with servers deployed in private networks.
> Consider validating the setup with your security department
> if you want to enable this plugin at a public Appium server deployment.
## Installation
```bash
appium plugin install storage
```
## Usage
Add the plugin name to the list of plugins to use upon server startup:
```bash
appium --use-plugins=storage
```
By default, the plugin creates a new temporary folder where it manages uploaded files.
[Refer to the Appium documentation for a list of commands supported by this plugin.](https://appium.io/docs/en/latest/commands/storage-plugin/)
### Storing a File
The procedure for storing a local file on the Appium server is as follows:
- Calculate the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash of the source file
- Decide the name of the destination file in the server storage (it can be the same as the original file name)
- Send a `POST` request to the `/storage/add` endpoint, which will return the `events` and `stream` websocket paths
- Connect to both web sockets
- Start listening for messages on the `events` web socket. Each message there is a JSON object wrapped
to a string. The message must be either `{"value": {"success": true, "name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}}` to notify about a successful
file upload, or `{"value": {"error": "<error signature>", "message": "<error message>", "traceback": "<server traceback>"}}`
to notify about any exceptions during the upload process.
- Start reading the source file into small chunks. The recommended size of a single chunk is 64 KB.
- After each chunk is retrieved, pass it to the `stream` web socket.
- After the last chunk upload is completed, either close the `stream` web
socket to explicitly notify the server about the upload completion, or
wait until the success event is delivered from the `events` web socket
as soon as file hashes successfully match.
The server must always deliver either a success or a failure
event via the `events` web socket as described above.
It is also possible to upload multiple files in parallel (up to 20 jobs are supported).
Only flat files hierarchies are supported in the storage, no subfolders are allowed.
If a file with the same name already exists in the storage, it will be overridden with the new one.
If a folder with the same name already exists in the storage, an error will be thrown.
### Environment Variables
#### APPIUM_STORAGE_ROOT
It is also possible to customize the repository root folder path by assigning a custom path to the
`APPIUM_STORAGE_ROOT` environment variable upon server startup. The plugin automatically deletes the
root folder recursively upon server process termination, unless the server is
killed forcefully. If `APPIUM_STORAGE_ROOT` points to an existing folder,
then all files there are going to be preserved by default unless a different behavior is
requested by [APPIUM_STORAGE_KEEP_ALL](#appium_storage_keep_all) environment variable value.
#### APPIUM_STORAGE_KEEP_ALL
If this environment variable is set to `true`, `1` or `yes` then the plugin will always keep
storage files after the server process is terminated. All other
values of this variable enforce the plugin to always delete all files
from the storage folder.
## Examples
Check [integration tests](./test/e2e/storage.e2e.spec.cjs) for a working
[WebdriverIO](https://webdriver.io/) example.
## License
Apache-2.0
+2
View File
@@ -0,0 +1,2 @@
export { StoragePlugin } from './plugin';
export type * from './types';
+205
View File
@@ -0,0 +1,205 @@
import { BasePlugin } from 'appium/plugin';
import { requireValidItemOptions, Storage, StorageArgumentError } from './storage';
import _ from 'lodash';
import { tempDir, fs, logger } from '@appium/support';
import { AddRequestResult, ItemOptions, StorageItem } from './types';
import type { Express, Request, Response } from 'express';
import { toW3cResponseError } from './utils';
import { AppiumServer } from '@appium/types';
import { LRUCache } from 'lru-cache';
import WebSocket from 'ws';
import { EventEmitter } from 'node:stream';
const log = logger.getLogger('StoragePlugin');
let SHARED_STORAGE: Storage | null = null;
const STORAGE_PREFIX = '/storage';
const WS_TTL_MS = 5 * 60 * 1000;
const STORAGE_HANDLERS: Record<string, (req: Request, httpServer?: AppiumServer) => Promise<any>> = {};
const STORAGE_ADDITIONS_CACHE: LRUCache<string, () => any> = new LRUCache({
max: 20,
ttl: WS_TTL_MS,
dispose: (f: () => any) => f(),
});
export class StoragePlugin extends BasePlugin {
static async updateServer(expressApp: Express, httpServer: AppiumServer): Promise<void> {
const buildHandler = (methodName: string) => async (req: Request, res: Response) => {
let status = 200;
let body: any;
try {
const value = await STORAGE_HANDLERS[methodName](req, httpServer);
body = {value: value ?? null};
} catch (e) {
[status, body] = toW3cResponseError(e);
}
log.debug(
`Responding to ${methodName} with ${_.truncate(JSON.stringify(body.value), {length: 200})}`
);
res.set('content-type', 'application/json; charset=utf-8');
res.status(status).send(body);
};
expressApp.post(`${STORAGE_PREFIX}/add`, buildHandler(STORAGE_HANDLERS.addStorageItem.name));
expressApp.get(`${STORAGE_PREFIX}/list`, buildHandler(STORAGE_HANDLERS.listStorageItems.name));
expressApp.post(`${STORAGE_PREFIX}/reset`, buildHandler(STORAGE_HANDLERS.resetStorage.name));
expressApp.post(`${STORAGE_PREFIX}/delete`, buildHandler(STORAGE_HANDLERS.deleteStorageItem.name));
}
}
STORAGE_HANDLERS.addStorageItem = async function addStorageItem(
req: Request, httpServer: AppiumServer
): Promise<AddRequestResult> {
const itemOptions = requireValidItemOptions(
parseRequestArgs(req, ['name', 'sha1']) as ItemOptions
);
const [stream, events] = prepareWebSockets(httpServer, itemOptions);
return {
ws: {
stream,
events,
},
ttlMs: WS_TTL_MS,
};
};
STORAGE_HANDLERS.listStorageItems = async function listStorageItems(): Promise<StorageItem[]> {
return await executeStorageMethod(
async (storage: Storage) => await storage.list()
);
};
STORAGE_HANDLERS.deleteStorageItem = async function deleteStorageItem(req: Request): Promise<boolean> {
return await executeStorageMethod(
async (storage: Storage) => await storage.delete(parseRequestArgs(req, ['name']).name)
);
};
STORAGE_HANDLERS.resetStorage = async function resetStorage(): Promise<void> {
await executeStorageMethod(
async (storage: Storage) => await storage.reset()
);
};
async function executeStorageMethod<T>(method: (storage: Storage) => Promise<T>): Promise<T> {
const storage = await getStorageSingleton();
return await method(storage);
}
function parseRequestArgs(req: Request, requiredKeys: string[]): Record<string, any> {
if (!_.isPlainObject(req.body)) {
throw new StorageArgumentError(`The request body must be a valid JSON object`);
}
for (const key of requiredKeys) {
if (!_.keys(req.body).includes(key)) {
throw new StorageArgumentError(
`The required argument '${key}' is missing (expected ${JSON.stringify(requiredKeys)})`
);
}
}
return req.body;
}
function prepareWebSockets(httpServer: AppiumServer, itemOptions: ItemOptions): [string, string] {
const commonPathname = `${STORAGE_PREFIX}/add/${itemOptions.sha1}`;
const streamPathname = `${commonPathname}/stream`;
const eventsPathname = `${commonPathname}/events`;
if (!_.isEmpty(httpServer.getWebSocketHandlers(streamPathname))) {
return [streamPathname, eventsPathname];
}
const streamServer = new WebSocket.Server({
noServer: true,
});
const eventsServer = new WebSocket.Server({
noServer: true,
});
const signaler = new EventEmitter();
const streamDoneCallback = () => {
log.debug(`Unmounting stream and events web sockets at ${commonPathname}`);
httpServer.removeWebSocketHandler(streamPathname);
httpServer.removeWebSocketHandler(eventsPathname);
setTimeout(() => {
streamServer.close();
eventsServer.close();
signaler.removeAllListeners();
}, 100);
};
STORAGE_ADDITIONS_CACHE.set(itemOptions.sha1, streamDoneCallback);
eventsServer.on('connection', async (wsUpstream: WebSocket) => {
signaler.on('status', (value) => wsUpstream.send(JSON.stringify(value)));
});
eventsServer.on('error', (e) => {
log.info(`The ${eventsPathname} web socket server has notified about an error: ${e.message}`);
});
streamServer.on('connection', async (wsUpstream: WebSocket) => {
log.info(`Starting a new server storage upload of '${itemOptions.name}' at ${streamPathname}`);
const storage = await getStorageSingleton();
try {
await storage.add(itemOptions, wsUpstream);
const successEvent = {
value: {
success: true,
...itemOptions,
}
};
log.debug(`Notifying about the successful addition of '${itemOptions.name}' to the server storage`);
signaler.emit('status', successEvent);
STORAGE_ADDITIONS_CACHE.delete(itemOptions.sha1);
} catch (e) {
log.debug(`Notifying about a failure while adding '${itemOptions.name}' to the server storage`);
// in case of a failure we do not want to close the server yet
// in anticipation of a retry
log.error(e);
const [, errorBody] = toW3cResponseError(e);
signaler.emit('status', errorBody);
}
});
streamServer.on('error', (e) => {
log.info(`The ${streamPathname} web socket server has notified about an error: ${e.message}`);
});
httpServer.addWebSocketHandler(streamPathname, streamServer);
httpServer.addWebSocketHandler(eventsPathname, eventsServer);
return [streamPathname, eventsPathname];
}
const getStorageSingleton = _.memoize(async () => {
let storageRoot: string;
let shouldPreserveRoot = false;
let shouldPreserveFiles = false;
if (process.env.APPIUM_STORAGE_ROOT) {
storageRoot = process.env.APPIUM_STORAGE_ROOT;
shouldPreserveRoot = shouldPreserveFiles = await fs.exists(storageRoot);
log.info(`Set '${storageRoot}' as the server storage root folder`);
} else {
storageRoot = await tempDir.openDir();
log.info(`Created '${storageRoot}' as the temporary server storage root folder`);
}
if (process.env.APPIUM_STORAGE_KEEP_ALL) {
shouldPreserveFiles = ['true', '1', 'yes'].includes(_.toLower(process.env.APPIUM_STORAGE_KEEP_ALL));
}
if (shouldPreserveFiles) {
log.info(`All server storage items will be always preserved unless deleted explicitly`);
} else {
log.info(
`All server storage items will be cleaned up automatically from '${storageRoot}' after ` +
`Appium server termination`
);
}
SHARED_STORAGE = new Storage(
storageRoot,
shouldPreserveRoot,
shouldPreserveFiles,
log,
);
await SHARED_STORAGE.reset();
return SHARED_STORAGE;
});
process.once('exit', () => {
SHARED_STORAGE?.cleanupSync();
});
export default StoragePlugin;
+276
View File
@@ -0,0 +1,276 @@
import { fs, timing } from '@appium/support';
import _ from 'lodash';
import B from 'bluebird';
import type { Path } from 'path-scurry';
import path from 'node:path';
import type { Stats } from 'node:fs';
import nativeFs from 'node:fs';
import { rimrafSync } from 'rimraf';
import { ItemOptions, StorageItem } from './types';
import AsyncLock from 'async-lock';
import type { AppiumLogger } from '@appium/types';
import type Stream from 'node:stream';
import type WebSocket from 'ws';
import { createHash } from 'node:crypto';
const MAX_TASKS = 5;
const TMP_EXT = '.filepart';
const ADDITION_LOCK = new AsyncLock();
const WS_SERVER_ERROR = 1011;
const SHA1_HASH_LEN = 40;
export class Storage {
private readonly _root: string;
private readonly _log: AppiumLogger;
private readonly _shouldPreserveRoot: boolean;
private readonly _shouldPreserveFiles: boolean;
constructor(
root: string,
shouldPreserveRoot: boolean,
shouldPreserveFiles: boolean,
log: AppiumLogger,
) {
this._root = root;
this._log = log;
this._shouldPreserveRoot = shouldPreserveRoot;
this._shouldPreserveFiles = shouldPreserveFiles;
}
async list(): Promise<StorageItem[]> {
const items = (await this._listFiles())
.filter((p) => !p.fullpath().endsWith(TMP_EXT));
if (_.isEmpty(items)) {
return [];
}
const statPromises: B<Stats>[] = [];
for (const item of items) {
const pending = statPromises.filter((p) => p.isPending());
if (pending.length >= MAX_TASKS) {
await B.any(pending);
}
statPromises.push(B.resolve(fs.stat(item.fullpath())));
}
return _.zip(
items.map((f) => f.fullpath()),
await B.all(statPromises)
)
.map(([fullpath, stat]) => ({
name: path.basename(fullpath as string),
path: fullpath as string,
size: (stat as Stats).size,
}));
}
async add(opts: ItemOptions, source: Stream | WebSocket): Promise<void> {
const {name} = requireValidItemOptions(opts);
// _.toLower is needed for case-insensitive server filesystems
await ADDITION_LOCK.acquire(_.toLower(name), async () => {
if (_.isFunction((source as any).pipe)) {
await this._addFromStream(opts, source as Stream);
} else {
await this._addFromWebSocket(opts, source as WebSocket);
}
});
}
async delete(name: string): Promise<boolean> {
if (_.toLower(name).endsWith(TMP_EXT)) {
return false;
}
const destinationPath = path.join(this._root, name);
if (!await fs.exists(destinationPath)) {
return false;
}
await fs.rimraf(destinationPath);
return true;
}
async reset(): Promise<void> {
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
await fs.rimraf(this._root);
}
if (!await fs.exists(this._root)) {
await fs.mkdirp(this._root);
return;
}
const files = (await this._listFiles())
.map((p) => p.fullpath())
.filter(
(fullPath) => !this._shouldPreserveFiles || _.toLower(path.basename(fullPath)).endsWith(TMP_EXT)
);
if (_.isEmpty(files)) {
return;
}
const promises: B<any>[] = [];
for (const fullPath of files) {
const pending = promises.filter((p) => p.isPending());
if (pending.length >= MAX_TASKS) {
await B.any(pending);
}
promises.push(B.resolve(fs.rimraf(fullPath)));
}
await B.all(promises);
}
cleanupSync(): void {
this._log.debug(`Cleaning up the '${this._root}' server storage folder`);
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
rimrafSync(this._root);
return;
}
let itemNames: string[];
try {
itemNames = nativeFs.readdirSync(this._root)
.filter((name) => !_.startsWith(name, '.'));
} catch (e) {
this._log.warn(
`Cannot list the '${this._root}' server storage folder. Original error: ${e.message}. ` +
`Skipping the cleanup.`
);
return;
}
if (_.isEmpty(itemNames)) {
if (!this._shouldPreserveRoot) {
rimrafSync(this._root);
}
return;
}
const matchedNames = itemNames.filter(
(name) => !this._shouldPreserveFiles || _.toLower(name).endsWith(TMP_EXT)
);
for (const matchedName of matchedNames) {
rimrafSync(path.join(this._root, matchedName));
}
if (!this._shouldPreserveRoot && _.isEmpty(_.without(itemNames, ...matchedNames))) {
rimrafSync(this._root);
}
}
private async _listFiles(): Promise<Path[]> {
const paths = (await fs.glob('*', {
cwd: this._root,
withFileTypes: true,
})) as unknown as Path[];
return paths.filter((item) => item.isFile());
}
private async _addFromStream(opts: ItemOptions, source: Stream): Promise<void> {
const {name} = opts;
const fullPath = path.join(this._root, toTempName(name));
const timer = new timing.Timer().start();
const destination = fs.createWriteStream(fullPath);
source.pipe(destination);
try {
await new Promise((resolve, reject) => {
destination.once('finish', () => resolve(true));
source.once('error', reject);
destination.once('error', reject);
});
await this._finalizeItem(opts, timer, fullPath, await fs.hash(fullPath));
} catch (e) {
await fs.rimraf(fullPath);
throw e;
}
}
private async _addFromWebSocket(opts: ItemOptions, source: WebSocket): Promise<void> {
const {name, sha1} = opts;
const fullPath = path.join(this._root, toTempName(name));
const timer = new timing.Timer().start();
const destination = fs.createWriteStream(fullPath);
const sha1sum = createHash('sha1');
let didDigestMatch = false;
let recentDigest: string | null = null;
try {
await new Promise((resolve, reject) => {
source.on('message', (data: WebSocket.RawData) => {
if (didDigestMatch) {
// ignore further chunks if hashes have already matched
return;
}
destination.write(data, (e) => {
if (e) {
source.close(WS_SERVER_ERROR);
reject(e);
}
});
sha1sum.update(data as any);
recentDigest = sha1sum.copy().digest('hex');
if (_.toLower(recentDigest) === _.toLower(sha1)) {
didDigestMatch = true;
destination.close(() => resolve(true));
}
});
source.once('close', () => {
destination.close(() => resolve(true));
});
source.once('error', reject);
destination.once('error', (e) => {
source.close(WS_SERVER_ERROR);
reject(e);
});
});
await this._finalizeItem(opts, timer, fullPath, recentDigest ?? sha1sum.digest('hex'));
} catch (e) {
await fs.rimraf(fullPath);
throw e;
}
}
private async _finalizeItem(
opts: ItemOptions,
timer: timing.Timer,
fullPath: string,
actualHashDigest: string,
): Promise<void> {
const {name, sha1} = opts;
this._log.info(
`'${name}' has been added to the server storage within ` +
`${timer.getDuration().asMilliSeconds}ms. Verifying hashes.`
);
if (_.toLower(actualHashDigest) !== _.toLower(sha1)) {
throw new StorageArgumentError(
`The actual SHA1 hash value '${actualHashDigest}' must be equal ` +
`to the expected hash value of '${sha1}' for '${name}'`
);
}
await fs.mv(fullPath, path.join(this._root, name));
}
}
function toTempName(origName: string): string {
return `${origName}${TMP_EXT}`;
}
export function requireValidItemOptions(opts: ItemOptions): ItemOptions {
if (_.isEmpty(opts.name)) {
throw new StorageArgumentError(`The provided file name '${opts.name}' must not be empty`);
}
const sanitizedName = fs.sanitizeName(opts.name, {
replacement: '_',
});
if (opts.name !== sanitizedName) {
throw new StorageArgumentError(
`The provided name value '${opts.name}' must be a valid file name. ` +
`Did you mean '${sanitizedName}'?`
);
}
if (!_.isString(opts.sha1) || opts.sha1.length !== SHA1_HASH_LEN) {
throw new StorageArgumentError(
`The provided hash value '${opts.sha1}' must be a valid SHA1 string, for ` +
`example 'ccc963411b2621335657963322890305ebe96186'`
);
}
return opts;
}
export class StorageArgumentError extends Error {}
+29
View File
@@ -0,0 +1,29 @@
export interface ItemOptions {
/** Destination file name */
name: string;
/** SHA1 hash of the file */
sha1: string;
}
export interface StorageItem {
/** Item name */
name: string;
/** Full path to the item on the server FS */
path: string;
/** Item size in bytes */
size: number;
}
export interface WsEndpoints {
/** Websocket pathname used to upload file chunks */
stream: string;
/** Websocket pathname used to deliver events to the client */
events: string;
}
export interface AddRequestResult {
/** The websocket pathname where the further upload is expected */
ws: WsEndpoints;
/** For how long the websocket server is going to be alive in milliseconds */
ttlMs: number;
}
+14
View File
@@ -0,0 +1,14 @@
import _ from 'lodash';
import { errors } from 'appium/driver';
export function toW3cResponseError(err: any): [number, Record<string, any>] {
const protocolError = _.has(err, 'w3cStatus') ? err : new errors.UnknownError(err);
return [protocolError.w3cStatus, {
value: {
error: protocolError.error,
message: protocolError.message,
stacktrace: protocolError.stacktrace || protocolError.stack,
},
}];
}
+71
View File
@@ -0,0 +1,71 @@
{
"name": "@appium/storage-plugin",
"version": "0.1.0",
"description": "Server-side storage plugin",
"keywords": [
"automation",
"javascript",
"selenium",
"webdriver",
"ios",
"android",
"firefoxos",
"testing"
],
"homepage": "https://appium.io",
"bugs": {
"url": "https://github.com/appium/appium/issues"
},
"main": "./build/lib/index.js",
"types": "./build/lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/appium/appium.git",
"directory": "packages/storage-plugin"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"directories": {
"lib": "./lib"
},
"files": [
"lib",
"build",
"tsconfig.json",
"!build/tsconfig.tsbuildinfo"
],
"scripts": {
"test": "npm run test:unit",
"test:smoke": "node ./build/lib/index.js",
"test:unit": "mocha \"./test/unit/**/*.spec.*js\"",
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.*js\""
},
"dependencies": {
"@appium/base-plugin": "^2.3.4",
"@appium/support": "^6.0.7",
"async-lock": "1.4.1",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
"rimraf": "5.0.10",
"source-map-support": "0.5.21",
"ws": "8.18.1"
},
"peerDependencies": {
"appium": "^2.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=8"
},
"appium": {
"pluginName": "storage",
"mainClass": "StoragePlugin"
},
"publishConfig": {
"access": "public"
},
"tags": [
"appium"
]
}
@@ -0,0 +1,168 @@
import _ from 'lodash';
import path from 'path';
import {remote as wdio} from 'webdriverio';
import {pluginE2EHarness} from '@appium/plugin-test-support';
import {tempDir, fs} from '@appium/support';
import axios from 'axios';
import { WebSocket } from 'ws';
const BUFFER_SIZE = 0xFFFF;
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
const TEST_HOST = '127.0.0.1';
const TEST_PORT = 4725;
const TEST_FAKE_APP = path.join(
APPIUM_HOME,
'node_modules',
'@appium',
'fake-driver',
'test',
'fixtures',
'app.xml'
);
const TEST_CAPS = {
platformName: 'Fake',
'appium:automationName': 'Fake',
'appium:deviceName': 'Fake',
'appium:app': TEST_FAKE_APP,
};
const WDIO_OPTS = {
hostname: TEST_HOST,
port: TEST_PORT,
connectionRetryCount: 0,
capabilities: TEST_CAPS,
};
describe('StoragePlugin', function () {
let server;
let driver;
/** @type {string | undefined | undefined} */
let storageRoot;
beforeEach(async function () {
storageRoot = await tempDir.openDir();
driver = await wdio(WDIO_OPTS);
const baseUrl = `http://${TEST_HOST}:${TEST_PORT}/storage`;
driver.addCommand(
'addStorageItem',
async (name, sha1) => (await axios.post(
`${baseUrl}/add`, {name, sha1}
)).data.value
);
driver.addCommand(
'listStorageItems',
async () => (await axios.get(`${baseUrl}/list`)).data.value
);
driver.addCommand(
'resetStorageItems',
async () => (await axios.post(`${baseUrl}/reset`)).data.value
);
driver.addCommand(
'deleteStorageItem',
async (name) => (await axios.post(`${baseUrl}/delete`, {name})).data.value
);
});
afterEach(async function () {
if (driver) {
await driver.deleteSession();
driver = null;
}
if (storageRoot && await fs.exists(storageRoot)) {
await fs.rimraf(storageRoot);
storageRoot = null;
}
});
pluginE2EHarness({
before,
after,
server,
port: TEST_PORT,
host: TEST_HOST,
appiumHome: APPIUM_HOME,
driverName: 'fake',
driverSource: 'local',
driverSpec: FAKE_DRIVER_DIR,
pluginName: 'storage',
pluginSource: 'local',
pluginSpec: THIS_PLUGIN_DIR,
});
it('should manage storage files', async function () {
let items = await driver.listStorageItems();
_.isEmpty(items).should.be.true;
const name1 = path.basename('foo1.bar');
const name2 = path.basename('foo2.bar');
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
await Promise.all([
addFileToStorage(TEST_FAKE_APP, name1),
addFileToStorage(pkgPath, name2),
]);
items = await driver.listStorageItems();
items.length.should.eql(2);
_.isEqual(new Set(items.map(({name}) => name)), new Set([name1, name2])).should.be.true;
const isDeleted = await driver.deleteStorageItem(name1);
isDeleted.should.be.true;
items = await driver.listStorageItems();
items.length.should.eql(1);
items[0].name.should.eql(name2);
await driver.resetStorageItems();
items = await driver.listStorageItems();
items.length.should.eql(0);
});
async function addFileToStorage(sourcePath, name) {
const hash = await fs.hash(sourcePath);
const {size} = await fs.stat(sourcePath);
const {ws: {events, stream}} = await driver.addStorageItem(name, hash, sourcePath);
const streamWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${stream}`);
const eventsWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${events}`);
try {
await new Promise((resolve, reject) => {
streamWs.once('error', reject);
eventsWs.once('error', reject);
eventsWs.once('message', async (data) => {
let strData;
if (_.isBuffer(data)) {
strData = data.toString();
} else if (_.isString(data)) {
strData = data;
} else {
return;
}
try {
const {value} = JSON.parse(strData);
if (value?.success) {
resolve(true);
} else {
reject(new Error(JSON.stringify(value)));
}
} catch {}
});
streamWs.once('open', async () => {
const fhandle = await fs.openFile(sourcePath, 'r');
try {
let bytesRead = 0;
while (bytesRead < size) {
const bufferSize = Math.min(BUFFER_SIZE, size - bytesRead);
const buffer = Buffer.alloc(bufferSize);
await fhandle.read(buffer, 0, bufferSize, bytesRead);
streamWs.send(buffer);
bytesRead += bufferSize;
}
} catch (e) {
reject(e);
} finally {
await fhandle.close();
streamWs.close();
}
});
});
} finally {
streamWs.close();
eventsWs.close();
}
}
});
@@ -0,0 +1,124 @@
import _ from 'lodash';
import { Storage } from '../../lib/storage';
import { tempDir, fs, logger } from '@appium/support';
import path from 'node:path';
const log = logger.getLogger();
describe('storage', function () {
/** @type {string | undefined} */
let tmpRoot;
/** @type {Storage | undefined | null} */
let storage;
/** @type {string | undefined | null} */
let storageRoot;
before(async function () {
const chai = await import('chai');
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
chai.should();
tmpRoot = await tempDir.openDir();
});
after(async function () {
if (tmpRoot && await fs.exists(tmpRoot)) {
await fs.rimraf(tmpRoot);
}
});
beforeEach(async function () {
storageRoot = await tempDir.openDir();
});
afterEach(async function () {
if (storage) {
await storage.reset();
storage = null;
}
if (storageRoot && await fs.exists(storageRoot)) {
await fs.rimraf(storageRoot);
storageRoot = null;
}
});
it('should be initially empty', async function () {
storage = new Storage(storageRoot, false, false, log);
const files = await storage.list();
_.isEmpty(files).should.be.true;
(await storage.delete('foo')).should.be.false;
});
it('should reset all files if shouldPreserveFiles is not requested', async function () {
const name = 'foo.bar';
const tmpName = 'bar.baz.filepart';
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
storage = new Storage(storageRoot, true, false, log);
const files = await storage.list();
files.length.should.eql(1);
await storage.reset();
(await fs.exists(path.join(storageRoot, name))).should.be.false;
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
});
it('should only reset parital files if shouldPreserveFiles requested', async function () {
const name = 'foo.bar';
const tmpName = 'bar.baz.filepart';
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
storage = new Storage(storageRoot, true, true, log);
let files = await storage.list();
files.length.should.eql(1);
await storage.reset();
files = await storage.list();
files.length.should.eql(1);
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
});
it('should perform basic operations', async function () {
storage = new Storage(storageRoot, false, false, log);
const name = 'foo.bar';
const size = 1 * 1024 * 1024;
await addFileToStorage(name, size);
let files = await storage.list();
_.isEmpty(files).should.be.false;
files[0].name.should.eql(name);
files[0].size.should.eql(size);
files[0].path.should.eql(path.join(storageRoot, name));
(await storage.delete(name)).should.be.true;
files = await storage.list();
_.isEmpty(files).should.be.true;
});
it('should be reset and preserve the root', async function () {
storage = new Storage(storageRoot, true, false, log);
const name = 'foo.bar';
const size = 1 * 1024 * 1024;
await addFileToStorage(name, size);
await storage.reset();
const files = await storage.list();
_.isEmpty(files).should.be.true;
(await fs.exists(storageRoot)).should.be.true;
});
it('should be reset and preserve items', async function () {
storage = new Storage(storageRoot, false, true, log);
const name = 'foo.bar';
const size = 1 * 1024 * 1024;
await addFileToStorage(name, size);
await storage.reset();
const files = await storage.list();
_.isEmpty(files).should.be.false;
(await fs.exists(storageRoot)).should.be.true;
});
async function addFileToStorage(name, size) {
const dummyPath = path.join(tmpRoot, name);
await fs.writeFile(dummyPath, Buffer.alloc(size));
const sha1 = await fs.hash(dummyPath);
await storage.add({name, sha1}, fs.createReadStream(dummyPath));
return dummyPath;
}
});
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "build",
"checkJs": true
},
"extends": "@appium/tsconfig/tsconfig.plugin.json",
"include": ["lib"],
"references": [
{
"path": "../base-plugin"
}
]
}
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.0.7](https://github.com/appium/appium/compare/@appium/support@6.0.6...@appium/support@6.0.7) (2025-03-11)
**Note:** Version bump only for package @appium/support
## [6.0.6](https://github.com/appium/appium/compare/@appium/support@6.0.5...@appium/support@6.0.6) (2025-02-20)
**Note:** Version bump only for package @appium/support
## [6.0.5](https://github.com/appium/appium/compare/@appium/support@6.0.4...@appium/support@6.0.5) (2025-02-20)
**Note:** Version bump only for package @appium/support
## [6.0.4](https://github.com/appium/appium/compare/@appium/support@6.0.3...@appium/support@6.0.4) (2025-02-19)
### Bug Fixes
* Avoid "Unhandled rejection" error on mjpeg connection failure ([#20909](https://github.com/appium/appium/issues/20909)) ([3c58054](https://github.com/appium/appium/commit/3c58054a476fbb92927ffa54732d1995e1b956c3))
* **support:** update dependency form-data to v4.0.2 ([#20997](https://github.com/appium/appium/issues/20997)) ([224f524](https://github.com/appium/appium/commit/224f5247b0cd4b7dcc392567b90d53b9b57a8b88))
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
* **support:** update dependency uuid to v11.0.5 ([#20896](https://github.com/appium/appium/issues/20896)) ([561aff6](https://github.com/appium/appium/commit/561aff641a4f62c82865ea036b6e1ca1bb89fb44))
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
## [6.0.3](https://github.com/appium/appium/compare/@appium/support@6.0.2...@appium/support@6.0.3) (2025-01-08)
**Note:** Version bump only for package @appium/support
+28 -27
View File
@@ -116,22 +116,23 @@ class MJpegStream extends Writable {
await initMJpegConsumer();
this.consumer = new MJpegConsumer();
// use the deferred pattern so we can wait for the start of the stream
// based on what comes in from an external pipe
const startPromise = new B((res, rej) => {
this.registerStartSuccess = res;
this.registerStartFailure = rej;
})
// start a timeout so that if the server does not return data, we don't
// block forever.
.timeout(
serverTimeout,
`Waited ${serverTimeout}ms but the MJPEG server never sent any images`
);
const url = this.url;
const onErr = (err) => {
try {
this.responseStream = (
await axios({
url,
responseType: 'stream',
timeout: serverTimeout,
})
).data;
} catch (e) {
throw new Error(
`Cannot connect to the MJPEG stream at ${url}. ` +
`Original error: ${_.has(e, 'response') ? JSON.stringify(e.response) : /** @type {Error} */ (e).message}`
);
}
const onErr = (/** @type {Error} */ err) => {
// Make sure we don't get an outdated screenshot if there was an error
this.lastChunk = null;
@@ -145,18 +146,18 @@ class MJpegStream extends Writable {
log.debug(`The connection to MJPEG server at ${url} has been closed`);
this.lastChunk = null;
};
try {
this.responseStream = (
await axios({
url,
responseType: 'stream',
timeout: serverTimeout,
})
).data;
} catch (e) {
return onErr(e);
}
// use the deferred pattern so we can wait for the start of the stream
// based on what comes in from an external pipe
const startPromise = new B((res, rej) => {
this.registerStartSuccess = res;
this.registerStartFailure = rej;
})
// start a timeout so that if the server does not return data, we don't
// block forever.
.timeout(
serverTimeout,
`Waited ${serverTimeout}ms but the MJPEG server never sent any images`
);
this.responseStream
.once('close', onClose)

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