Compare commits

...
Author SHA1 Message Date
github-actions eb5ce47b17 chore: publish
- appium@3.8.0
 - @appium/base-driver@10.8.1
 - @appium/base-plugin@3.3.5
 - @appium/docutils@3.0.1
 - @appium/driver-test-support@3.0.1
 - @appium/eslint-config-appium-ts@3.2.6
 - @appium/execute-driver-plugin@7.0.1
 - @appium/fake-driver@6.2.4
 - @appium/fake-plugin@4.4.1
 - @appium/images-plugin@5.0.1
 - @appium/logger@2.0.12
 - @appium/opencv@5.0.1
 - @appium/oxc-config@1.1.2
 - @appium/plugin-test-support@3.0.1
 - @appium/relaxed-caps-plugin@3.0.1
 - @appium/storage-plugin@2.0.1
 - @appium/strongbox@2.0.1
 - @appium/support@7.2.8
 - @appium/types@1.7.1
 - @appium/universal-xml-plugin@3.0.1
2026-09-24 18:25:54 +00:00
Mykola Mokhnach 7df378f401 fix(support): avoid TOCTOU race in getLockFileGuard() acquisition (#22807) 2026-09-23 08:42:29 +02:00
Mohamed Mostafa 4dd2e2d47d fix(support): do not delete a path when fs.mv destination is the source or inside it (#22806) 2026-09-23 08:35:12 +02:00
renovate[bot] dacc42ba0a chore(storage-plugin): update dependency lru-cache to v11.5.3 (#22802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-22 19:36:17 +00:00
renovate[bot] 2502f538ff chore(deps): update dependency @types/node to v24.13.6 (#22801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-22 16:29:05 +00:00
renovate[bot] 3f38f5cd74 chore(types): update dependency type-fest to v5.10.0 (#22798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 23:31:41 +00:00
renovate[bot] 6891dd4eae chore(oxc-config): update dependency oxlint-tsgolint to v7.0.2002 (#22797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-21 15:15:48 +00:00
Mykola Mokhnach 4e8e334cbc fix(appium): filter prerelease versions in driver update e2e test (#22794) 2026-09-20 15:15:22 +02:00
Mohamed Mostafa 3423e36689 fix(base-driver): keep existing contexts when session.subscribe is called again (#22780) 2026-09-20 14:19:29 +02:00
renovate[bot] 14a9d98641 chore(eslint-config-appium-ts): update dependency eslint-plugin-perfectionist to v5.11.1 (#22786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 18:08:51 +02:00
renovate[bot] f79fafb816 chore(deps): update dependency @types/node to v24.13.5 (#22785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 10:22:06 +00:00
Edgars Eglītis 443b0bdd03 chore(oxc-config): update oxc packages (#22787) 2026-09-19 12:38:39 +03:00
renovate[bot] 80fbf957c8 chore(docutils): update dependency mkdocs-git-revision-date-localized-plugin to v1.6.0 (#22782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-18 23:43:43 +00:00
renovate[bot] 0443c84ec0 chore(execute-driver-plugin): update dependency webdriverio to v9.31.9 (#22781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-18 08:43:04 +00:00
renovate[bot] a47fdb8e05 chore(execute-driver-plugin): update dependency webdriverio to v9.31.8 (#22779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-16 17:52:21 +02:00
renovate[bot] e2c8f09bb9 chore(docutils): update dependency yaml to v2.9.1 (#22778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-16 17:52:08 +02:00
Mohamed Mostafa dc16d6970f fix(support): extract zip entries whose names start with dots (#22772) 2026-09-14 18:08:04 +02:00
renovate[bot] abfe1243a1 chore(base-driver): update dependency morgan to v1.12.1 (#22775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-14 13:44:06 +02:00
renovate[bot] 9bbf6b72bf chore(appium): update dependency argparse to v3.0.2 (#22773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-14 11:27:58 +00:00
renovate[bot] 0fa9c9bd80 chore(deps): update dependency @types/node to v24.13.4 (#22768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-13 13:46:08 +00:00
Mohamed Mostafa 9a70a44c83 fix(support): do not set file-sized Content-Length on default multipart uploads (#22766) 2026-09-12 19:02:21 +02:00
renovate[bot] 65e14685a8 chore(oxc-config): update dependency oxfmt to v0.67.0 (#22724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-12 11:50:40 +03:00
renovate[bot] 29eeb28f86 chore(oxc-config): update dependency oxlint to v1.82.0 (#22726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-12 06:39:44 +00:00
github-actions[bot]andmykola-mokhnach ac412b8723 docs(appium): Update documentation translations (#22759)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2026-09-11 19:36:59 +03:00
renovate[bot] 68740320d2 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.70.0 (#22761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 12:14:22 +00:00
renovate[bot] 9dc6cd0019 chore(execute-driver-plugin): update dependency webdriverio to v9.31.7 (#22760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 12:04:55 +00:00
Mohamed Mostafa 81c8b9da0a fix(base-driver): honor unquoted and RFC 5987 Content-Disposition filenames (#22755) 2026-09-11 08:19:50 +02:00
renovate[bot] 726348c2d5 chore(js): update dependency webdriverio to v9.31.6 (#22751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-10 16:32:41 +00:00
Jack VanSickle 6c911debd7 feat(appium): accept package names for installed extension commands (#22747) 2026-09-10 07:25:36 +02:00
Edgars Eglītis 8ccb436701 chore(deps): bump ESLint and bplist-related dependencies (#22748) 2026-09-09 11:22:33 +03:00
Mohamed Mostafa f522b64456 fix(storage-plugin): map StorageArgumentError to InvalidArgumentError (#22734) 2026-09-08 06:50:51 +02:00
Mohamed Mostafa 3180558086 test(images-plugin): give the unit tests room to retry and finish on CI (#22743) 2026-09-08 06:44:12 +02:00
dependabot[bot] 9d07f6bc5e chore(deps): bump deepmerge-ts (#22739)
Bumps the npm_and_yarn group with 1 update in the / directory: [deepmerge-ts](https://github.com/RebeccaStevens/deepmerge-ts).


Updates `deepmerge-ts` from 7.1.6 to 8.0.2
- [Release notes](https://github.com/RebeccaStevens/deepmerge-ts/releases)
- [Changelog](https://github.com/RebeccaStevens/deepmerge-ts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/RebeccaStevens/deepmerge-ts/compare/v7.1.6...v8.0.2)

---
updated-dependencies:
- dependency-name: deepmerge-ts
  dependency-version: 8.0.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 12:42:56 +03:00
Edgars Eglītis b234e2b56b chore(oxc-config): bump oxc dependencies (#22737) 2026-09-07 12:11:56 +03:00
Edgars Eglītis b635641f24 chore: regenerate lockfile (#22728)
* chore: regenerate lockfile

* chore: regenerate again after a deeper cleanup
2026-09-05 14:10:02 +03:00
Edgars EglītisandMykola Mokhnach 6d720b1112 test(base-driver): fix invalid import (#22729)
* test: fix invalid import

* test: import BaseDriver from package entry in type test

tsd builds its type-check program from an explicit file list, which
trips TS6059 (composite project) when the raw lib source is imported
directly, pulling in the whole transitive dependency tree.

* fix: exclude test/types from base-driver's main tsc build

Importing @appium/base-driver by package name from within its own
test/types dir is a self-reference: the main composite build (tsc -b)
was including test/types too, so it tried to resolve the package's own
not-yet-emitted build/lib/index.d.ts while still generating it,
breaking the whole build. packages/strongbox already excludes
test/types from its main include for the same reason; tsd targets
test/types independently via its own "directory" config, unaffected
by this exclude.

* fix: use explicit tsconfig include instead of excluding test/types

Matches packages/strongbox's pattern of listing test subfolders
explicitly rather than excluding test/types from "test".

---------

Co-authored-by: Mykola Mokhnach <mokhnach@gmail.com>
2026-09-05 13:55:31 +03:00
Mohamed Mostafa 6ab17feb71 fix(appium): match insecure feature prefixes by automationName, case-insensitively (#22710) 2026-09-05 09:25:01 +02:00
Mohamed Mostafa afa4837530 fix(strongbox): reject item names that slugify to an empty path (#22717) 2026-09-04 18:37:47 +02:00
renovate[bot] 1ada93cf76 chore(types): update dependency type-fest to v5.9.0 (#22713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 11:03:14 +00:00
dependabot[bot] b32e28d36d chore(deps): bump fast-uri in the npm_and_yarn group across 1 directory (#22711)
Bumps the npm_and_yarn group with 1 update in the / directory: [fast-uri](https://github.com/fastify/fast-uri).


Updates `fast-uri` from 3.1.5 to 3.1.7
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 10:50:55 +03:00
renovate[bot] 119e21f067 chore(opencv): update dependency sharp to v0.35.4 (#22712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 07:40:13 +02:00
Mohamed Mostafa 6ba6faf6f5 fix(appium): do not treat a directory as a CLI argument file (#22709) 2026-09-02 20:40:47 +02:00
dependabot[bot] 422362a259 chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#22707)
Bumps the npm_and_yarn group with 3 updates in the / directory: [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node), [browserslist](https://github.com/browserslist/browserslist) and [qs](https://github.com/ljharb/qs).


Updates `@humanfs/node` from 0.16.6 to 0.16.8
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

Updates `browserslist` from 4.28.2 to 4.28.8
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

Updates `qs` from 6.15.2 to 6.16.0
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.2...v6.16.0)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: qs
  dependency-version: 6.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 19:30:56 +03:00
renovate[bot] 62bfe1566a chore(execute-driver-plugin): update dependency webdriverio to v9.31.5 (#22706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-02 15:28:34 +00:00
Kazuaki Matsuo 8a9350b8e9 docs: fix docs build (#22702) 2026-09-02 13:01:01 +09:00
github-actions[bot]andmykola-mokhnach bf51d27137 docs(appium): Update documentation translations (#22701)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2026-09-02 12:21:52 +09:00
dependabot[bot] de00df0afa chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#22698)
Bumps the npm_and_yarn group with 2 updates in the / directory: [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser) and [tar](https://github.com/isaacs/node-tar).


Updates `postcss-selector-parser` from 7.1.1 to 7.1.4
- [Release notes](https://github.com/postcss/postcss-selector-parser/releases)
- [Changelog](https://github.com/postcss/postcss-selector-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss-selector-parser/compare/v7.1.1...7.1.4)

Updates `tar` from 7.5.19 to 7.5.22
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.19...v7.5.22)

---
updated-dependencies:
- dependency-name: postcss-selector-parser
  dependency-version: 7.1.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar
  dependency-version: 7.5.22
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-01 19:21:41 +03:00
renovate[bot] c3a48181c8 chore(oxc-config): update dependency oxlint to v1.80.0 (#22696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 16:05:19 +00:00
Edgars Eglītis 77dc09af36 docs: add watchOS blogpost (#22697) 2026-09-01 18:57:25 +03:00
renovate[bot] 59af10a17c chore(oxc-config): update dependency oxfmt to v0.65.0 (#22695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 13:29:46 +03:00
renovate[bot] 6e989bd89a chore(support): update dependency axios to v1.20.0 (#22692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 10:09:07 +00:00
renovate[bot] 557e71f766 chore(deps): update dependency json-schema-to-typescript to v16 (#22694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 13:03:25 +03:00
renovate[bot] 63d94d03c6 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.68.0 (#22693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 00:43:50 +00:00
renovate[bot] 8a0022c147 chore(base-driver): update dependency morgan to v1.12.0 (#22690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 21:10:49 +00:00
renovate[bot] 72ee77d139 chore(universal-xml-plugin): update dependency fast-xml-parser to v5.11.1 (#22691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 21:05:10 +00:00
renovate[bot] 70a01da4b2 chore(support): update dependency asyncbox to v6.4.3 (#22688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 19:24:34 +00:00
renovate[bot] 1b705bcd24 chore(appium): update dependency argparse to v3.0.1 (#22689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 19:11:44 +00:00
renovate[bot] 574bd6cbfb chore(execute-driver-plugin): update dependency webdriverio to v9.31.4 (#22687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-31 19:06:33 +00:00
Mohamed Mostafa 9c00b86ce7 fix(base-driver): reject invalid timeout with InvalidArgumentError (#22679) 2026-08-31 20:35:57 +02:00
Mohamed Mostafa 0c4da64aea fix(base-driver): skip unknown events in session.unsubscribe (#22671) 2026-08-29 17:26:32 +02:00
Edgars Eglītis dafc5985bb ci: tune docs publish workflow (#22670) 2026-08-25 20:06:49 +03:00
github-actions[bot]andmykola-mokhnach 02443f98d6 docs(appium): Update documentation translations (#22669)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2026-08-25 14:16:56 +09:00
renovate[bot] b4097b54d4 chore(execute-driver-plugin): update dependency webdriverio to v9.31.2 (#22667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 21:28:14 +00:00
88 changed files with 5813 additions and 5069 deletions
+5 -6
View File
@@ -6,7 +6,8 @@ on:
# Runs when any new Appium tag is pushed = there is a new release
# Does not trigger for tags with a slash, e.g. 1.0/beta
tags:
- 'appium@*'
- appium@*
- '!appium@*beta*'
jobs:
build:
@@ -14,6 +15,8 @@ jobs:
steps:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0 # ensure the docs branch is also fetched
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
@@ -25,9 +28,5 @@ jobs:
run-build: 'true'
- name: Install dependencies (Python)
run: npm run install-docs-deps
- name: Fetch gh-pages
run: |
git fetch origin gh-pages:gh-pages || true
- name: Publish docs
run: |
npm run docs:publish
run: npm run docs:publish
+4553 -4882
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -93,7 +93,7 @@
"@types/lockfile": "1.0.4",
"@types/method-override": "3.0.0",
"@types/morgan": "1.9.10",
"@types/node": "24.13.3",
"@types/node": "24.13.6",
"@types/plist": "3.0.5",
"@types/pluralize": "0.0.33",
"@types/semver": "7.8.0",
@@ -103,12 +103,12 @@
"@types/which": "3.0.4",
"@types/ws": "8.18.1",
"@types/yargs": "17.0.35",
"asyncbox": "6.4.2",
"asyncbox": "6.4.3",
"conventional-changelog-conventionalcommits": "9.3.1",
"cpy-cli": "7.0.0",
"cross-env": "10.1.0",
"finalhandler": "2.1.1",
"json-schema-to-typescript": "15.0.4",
"json-schema-to-typescript": "16.0.0",
"lerna": "10.0.1",
"mjpeg-consumer": "2.0.0",
"mjpeg-server": "0.3.1",
@@ -120,7 +120,7 @@
"teen_process": "4.2.1",
"tsd": "0.33.0",
"typescript": "6.0.3",
"webdriverio": "9.31.1",
"webdriverio": "9.31.9",
"ws": "8.21.3"
},
"overrides": {
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.8.0](https://github.com/appium/appium/compare/appium@3.7.0...appium@3.8.0) (2026-09-24)
### Features
* **appium:** accept package names for installed extension commands ([#22747](https://github.com/appium/appium/issues/22747)) ([6c911de](https://github.com/appium/appium/commit/6c911debd72cb7fe094d27671d1cf14098ff537a))
### Bug Fixes
* **appium:** do not treat a directory as a CLI argument file ([#22709](https://github.com/appium/appium/issues/22709)) ([6ba6faf](https://github.com/appium/appium/commit/6ba6faf6f533192afbdf9462dbbb3a5c1e5d85c5))
* **appium:** filter prerelease versions in driver update e2e test ([#22794](https://github.com/appium/appium/issues/22794)) ([4e8e334](https://github.com/appium/appium/commit/4e8e334cbc0ae910f521ae7ff7d3ed15f9be0a6a))
* **appium:** match insecure feature prefixes by automationName, case-insensitively ([#22710](https://github.com/appium/appium/issues/22710)) ([6ab17fe](https://github.com/appium/appium/commit/6ab17feb71d74c1f349616f22896faeb256a66ec))
## [3.7.0](https://github.com/appium/appium/compare/appium@3.6.0...appium@3.7.0) (2026-08-24)
### Features
@@ -17,3 +17,8 @@ authors:
avatar: 'https://avatars.githubusercontent.com/u/4045041?v=4'
slug: 'sai'
url: 'https://saikrishna.tech'
mykola:
name: 'Mykola Mokhnach'
description: 'Appium Core Maintainer'
avatar: https://avatars.githubusercontent.com/u/7767781?v=4
slug: 'mykola'
@@ -0,0 +1,38 @@
---
authors:
- mykola
date: 2026-09-01
---
# Announcing watchOS Simulator Support in the XCUITest Driver
We’re excited to announce that the XCUITest driver now supports automation of watchOS apps — a
platform that’s been on Appium’s requested list for a long time.
<!-- more -->
In order to automate watchOS, you'll need the following:
* XCUITest driver version 12.6.0 or later (WebDriverAgent 16.5.0 or later)
* Xcode 15.4 or later
* watchOS 10 or later
Important to note: ^^the driver currently only works with simulators — real devices are not
compatible.^^
The good news: automating a watchOS app looks just like automating any other iOS/iPadOS app. The
standard use-cases of finding and clicking on elements, retrieving the page source, using the
Appium Inspector — all work as usual. The one exception here is standard W3C gesture/touch actions
(tap/swipe via the Actions API), which are not supported.
On top of that, a few watchOS-specific extensions are available:
- `mobile: pressButton` — press the Digital Crown or Action button
- `mobile: rotateDigitalCrown` — rotate the Digital Crown (since driver version 12.7.0)
- `mobile: performHandGesture` — perform double-tap or wrist flick hand gestures (since driver
version 12.7.0)
For full setup instructions, capabilities, and limitations, check out the
[watchOS guide](https://appium.github.io/appium-xcuitest-driver/latest/guides/watchos/).
Happy testing!
@@ -13,6 +13,12 @@ Provides management options for a specific extension (driver or plugin). Both th
The following sub-subcommands are supported: `doctor`, `install`, `list`, `run`, `update`,
and `uninstall`.
For `doctor`, `run`, `update`, and `uninstall`, you can identify an installed extension by its
short name or its exact npm package name. For example, `appium driver uninstall uiautomator2`
and `appium driver uninstall appium-uiautomator2-driver` refer to the same installed driver.
Short names take precedence if a name also matches another extension's package name. Package names
can include a scope, such as `@appium/fake-driver`, but must not include a version or tag suffix.
## `doctor`
Runs doctor checks for an installed extension, which validate whether the extension has its prerequisites
@@ -31,7 +37,7 @@ appium {driver|plugin} doctor <extension-name>
|Argument|Description|
|--|--|
|`extension-name`|The short name of the installed extension|
|`extension-name`|The short name or exact npm package name of the installed extension|
#### Options
@@ -163,7 +169,7 @@ appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
|Argument|Description|
|--|--|
|`extension-name`|The short name of the installed extension|
|`extension-name`|The short name or exact npm package name of the installed extension|
|`script-name`|The name of the script to run. If not provided, a list of available scripts is returned.|
|`script-args`|Any additional arguments passed to the script|
@@ -200,7 +206,7 @@ appium {driver|plugin} update <extension-name>
|Argument|Description|
|--|--|
|`extension-name`|The short name of the installed extension, or `installed` to update all installed extensions|
|`extension-name`|The short name or exact npm package name of the installed extension, or `installed` to update all installed extensions|
#### Options
@@ -235,7 +241,7 @@ appium {driver|plugin} uninstall <extension-name>
|Argument|Description|
|--|--|
|`extension-name`|The short name of the installed extension|
|`extension-name`|The short name or exact npm package name of the installed extension|
#### Options
@@ -17,3 +17,8 @@ authors:
avatar: 'https://avatars.githubusercontent.com/u/4045041?v=4'
slug: 'sai'
url: 'https://saikrishna.tech'
mykola:
name: 'Mykola Mokhnach'
description: 'Appium Core Maintainer'
avatar: https://avatars.githubusercontent.com/u/7767781?v=4
slug: 'mykola'
@@ -0,0 +1,38 @@
---
authors:
- mykola
date: 2026-09-01
---
# Announcing watchOS Simulator Support in the XCUITest Driver
We’re excited to announce that the XCUITest driver now supports automation of watchOS apps — a
platform that’s been on Appium’s requested list for a long time.
<!-- more -->
In order to automate watchOS, you'll need the following:
- XCUITest driver version 12.6.0 or later (WebDriverAgent 16.5.0 or later)
- Xcode 15.4 or later
- watchOS 10 or later
Important to note: ^^the driver currently only works with simulators — real devices are not
compatible.^^
The good news: automating a watchOS app looks just like automating any other iOS/iPadOS app. The
standard use-cases of finding and clicking on elements, retrieving the page source, using the
Appium Inspector — all work as usual. The one exception here is standard W3C gesture/touch actions
(tap/swipe via the Actions API), which are not supported.
On top of that, a few watchOS-specific extensions are available:
- `mobile: pressButton` — press the Digital Crown or Action button
- `mobile: rotateDigitalCrown` — rotate the Digital Crown (since driver version 12.7.0)
- `mobile: performHandGesture` — perform double-tap or wrist flick hand gestures (since driver
version 12.7.0)
For full setup instructions, capabilities, and limitations, check out the
[watchOS guide](https://appium.github.io/appium-xcuitest-driver/latest/guides/watchos/).
Happy testing!
+11 -1
View File
@@ -130,7 +130,7 @@ appium driver install --source=npm appium-flutter-integration-driver
appium driver install --source=npm appium-lg-webos-driver
```
### [Linux](https://github.com/fantonglang/appium-linux-driver)
### [Linux (@fantonglang)](https://github.com/fantonglang/appium-linux-driver)
!!! warning
@@ -149,6 +149,16 @@ yarn install
node ./
```
### [Linux (@Itsmeaj)](https://github.com/Itsmeaj/appium)
- Target: Linux desktop applications on X11 and Wayland
- Mode: Native
- Supported by: `@Itsmeaj`
```sh title="Install This Driver"
appium driver install --source=npm @itsmeaj/appium-linux-driver
```
### [NovaWindows](https://github.com/AutomateThePlanet/appium-novawindows-driver)
!!! info
@@ -14,6 +14,12 @@ Provides management options for a specific extension (driver or plugin). Both th
The following sub-subcommands are supported: `doctor`, `install`, `list`, `run`, `update`,
and `uninstall`.
For `doctor`, `run`, `update`, and `uninstall`, you can identify an installed extension by its
short name or its exact npm package name. For example, `appium driver uninstall uiautomator2`
and `appium driver uninstall appium-uiautomator2-driver` refer to the same installed driver.
Short names take precedence if a name also matches another extension's package name. Package names
can include a scope, such as `@appium/fake-driver`, but must not include a version or tag suffix.
## `doctor`
Runs doctor checks for an installed extension, which validate whether the extension has its prerequisites
@@ -30,9 +36,9 @@ configured correctly. Note that not all extensions include doctor checks.
appium {driver|plugin} doctor <extension-name>
```
| Argument | Description |
| ---------------- | ----------------------------------------- |
| `extension-name` | The short name of the installed extension |
| Argument | Description |
| ---------------- | ------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension |
#### Options
@@ -164,7 +170,7 @@ appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
| Argument | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `extension-name` | The short name of the installed extension |
| `extension-name` | The short name or exact npm package name of the installed extension |
| `script-name` | The name of the script to run. If not provided, a list of available scripts is returned. |
| `script-args` | Any additional arguments passed to the script |
@@ -199,9 +205,9 @@ Appium will only update minor and patch versions, in order to prevent any breaki
appium {driver|plugin} update <extension-name>
```
| Argument | Description |
| ---------------- | -------------------------------------------------------------------------------------------- |
| `extension-name` | The short name of the installed extension, or `installed` to update all installed extensions |
| Argument | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension, or `installed` to update all installed extensions |
#### Options
@@ -234,9 +240,9 @@ Removes an installed extension.
appium {driver|plugin} uninstall <extension-name>
```
| Argument | Description |
| ---------------- | ----------------------------------------- |
| `extension-name` | The short name of the installed extension |
| Argument | Description |
| ---------------- | ------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension |
#### Options
@@ -17,3 +17,8 @@ authors:
avatar: 'https://avatars.githubusercontent.com/u/4045041?v=4'
slug: 'sai'
url: 'https://saikrishna.tech'
mykola:
name: 'Mykola Mokhnach'
description: 'Appium Core Maintainer'
avatar: https://avatars.githubusercontent.com/u/7767781?v=4
slug: 'mykola'
@@ -0,0 +1,38 @@
---
authors:
- mykola
date: 2026-09-01
---
# Announcing watchOS Simulator Support in the XCUITest Driver
We’re excited to announce that the XCUITest driver now supports automation of watchOS apps — a
platform that’s been on Appium’s requested list for a long time.
<!-- more -->
In order to automate watchOS, you'll need the following:
- XCUITest driver version 12.6.0 or later (WebDriverAgent 16.5.0 or later)
- Xcode 15.4 or later
- watchOS 10 or later
Important to note: ^^the driver currently only works with simulators — real devices are not
compatible.^^
The good news: automating a watchOS app looks just like automating any other iOS/iPadOS app. The
standard use-cases of finding and clicking on elements, retrieving the page source, using the
Appium Inspector — all work as usual. The one exception here is standard W3C gesture/touch actions
(tap/swipe via the Actions API), which are not supported.
On top of that, a few watchOS-specific extensions are available:
- `mobile: pressButton` — press the Digital Crown or Action button
- `mobile: rotateDigitalCrown` — rotate the Digital Crown (since driver version 12.7.0)
- `mobile: performHandGesture` — perform double-tap or wrist flick hand gestures (since driver
version 12.7.0)
For full setup instructions, capabilities, and limitations, check out the
[watchOS guide](https://appium.github.io/appium-xcuitest-driver/latest/guides/watchos/).
Happy testing!
+11 -1
View File
@@ -130,7 +130,7 @@ appium driver install --source=npm appium-flutter-integration-driver
appium driver install --source=npm appium-lg-webos-driver
```
### [Linux](https://github.com/fantonglang/appium-linux-driver)
### [Linux (@fantonglang)](https://github.com/fantonglang/appium-linux-driver)
!!! warning
@@ -149,6 +149,16 @@ yarn install
node ./
```
### [Linux (@Itsmeaj)](https://github.com/Itsmeaj/appium)
- Target: Linux desktop applications on X11 and Wayland
- Mode: Native
- Supported by: `@Itsmeaj`
```sh title="Install This Driver"
appium driver install --source=npm @itsmeaj/appium-linux-driver
```
### [NovaWindows](https://github.com/AutomateThePlanet/appium-novawindows-driver)
!!! info
@@ -14,6 +14,12 @@ Provides management options for a specific extension (driver or plugin). Both th
The following sub-subcommands are supported: `doctor`, `install`, `list`, `run`, `update`,
and `uninstall`.
For `doctor`, `run`, `update`, and `uninstall`, you can identify an installed extension by its
short name or its exact npm package name. For example, `appium driver uninstall uiautomator2`
and `appium driver uninstall appium-uiautomator2-driver` refer to the same installed driver.
Short names take precedence if a name also matches another extension's package name. Package names
can include a scope, such as `@appium/fake-driver`, but must not include a version or tag suffix.
## `doctor`
Runs doctor checks for an installed extension, which validate whether the extension has its prerequisites
@@ -30,9 +36,9 @@ configured correctly. Note that not all extensions include doctor checks.
appium {driver|plugin} doctor <extension-name>
```
| Argument | Description |
| ---------------- | ----------------------------------------- |
| `extension-name` | The short name of the installed extension |
| Argument | Description |
| ---------------- | ------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension |
#### Options
@@ -164,7 +170,7 @@ appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
| Argument | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `extension-name` | The short name of the installed extension |
| `extension-name` | The short name or exact npm package name of the installed extension |
| `script-name` | The name of the script to run. If not provided, a list of available scripts is returned. |
| `script-args` | Any additional arguments passed to the script |
@@ -199,9 +205,9 @@ Appium will only update minor and patch versions, in order to prevent any breaki
appium {driver|plugin} update <extension-name>
```
| Argument | Description |
| ---------------- | -------------------------------------------------------------------------------------------- |
| `extension-name` | The short name of the installed extension, or `installed` to update all installed extensions |
| Argument | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension, or `installed` to update all installed extensions |
#### Options
@@ -234,9 +240,9 @@ Removes an installed extension.
appium {driver|plugin} uninstall <extension-name>
```
| Argument | Description |
| ---------------- | ----------------------------------------- |
| `extension-name` | The short name of the installed extension |
| Argument | Description |
| ---------------- | ------------------------------------------------------------------- |
| `extension-name` | The short name or exact npm package name of the installed extension |
#### Options
+5 -4
View File
@@ -164,7 +164,8 @@ function makeUninstallArgs(type: ExtensionType): ArgumentDefinitions {
{
type: 'str',
help:
`Name of the ${type} to uninstall, for example: ` + (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
`Short name or npm package name of the ${type} to uninstall, for example: ` +
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
},
],
]);
@@ -181,7 +182,7 @@ function makeDoctorArgs(type: ExtensionType): ArgumentDefinitions {
{
type: 'str',
help:
`Name of the ${type} to run doctor checks for, for example: ` +
`Short name or npm package name of the ${type} to run doctor checks for, for example: ` +
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
},
],
@@ -199,7 +200,7 @@ function makeUpdateArgs(type: ExtensionType): ArgumentDefinitions {
{
type: 'str',
help:
`Name of the ${type} to update, or "installed" to update all installed ${type}s. ` +
`Short name or npm package name of the ${type} to update, or "installed" to update all installed ${type}s. ` +
`To see available ${type} updates, run "appium ${type} list --installed --updates". ` +
'For example: ' +
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
@@ -228,7 +229,7 @@ function makeRunArgs(type: ExtensionType): ArgumentDefinitions {
{
type: 'str',
help:
`Name of the ${type} to run a script from, for example: ` +
`Short name or npm package name of the ${type} to run a script from, for example: ` +
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
},
],
+23 -5
View File
@@ -256,6 +256,20 @@ export abstract class ExtensionCliCommand<ExtType extends ExtensionType = Extens
return new Error(this.log.decorate(message, 'error'));
}
/**
* Resolves an installed npm package name to its extension name, preferring an exact
* extension name match. Leaves unknown names unchanged for the command's error message.
*/
protected _resolveExtensionName(installSpec: string): string {
if (this.config.isInstalled(installSpec)) {
return installSpec;
}
return (
Object.entries(this.config.installedExtensions).find(([, {pkgName}]) => pkgName === installSpec)?.[0] ??
installSpec
);
}
/**
* Build the initial list data structure from installed and known extensions
*/
@@ -418,7 +432,8 @@ export abstract class ExtensionCliCommand<ExtType extends ExtensionType = Extens
*
* @return map of all installed extension names to extension data (without the extension just uninstalled)
*/
protected async _uninstall({installSpec}: UninstallOpts): Promise<Record<string, any>> {
protected async _uninstall({installSpec: rawInstallSpec}: UninstallOpts): Promise<Record<string, any>> {
const installSpec = this._resolveExtensionName(rawInstallSpec);
if (!this.config.isInstalled(installSpec)) {
throw this._createFatalError(`Can't uninstall ${this.type} '${installSpec}'; it is not installed`);
}
@@ -439,8 +454,9 @@ export abstract class ExtensionCliCommand<ExtType extends ExtensionType = Extens
/**
* Attempt to update one or more drivers using NPM
*/
protected async _update({installSpec, unsafe}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
const shouldUpdateAll = installSpec === UPDATE_ALL;
protected async _update({installSpec: rawInstallSpec, unsafe}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
const shouldUpdateAll = rawInstallSpec === UPDATE_ALL;
const installSpec = shouldUpdateAll ? rawInstallSpec : this._resolveExtensionName(rawInstallSpec);
// if we're specifically requesting an update for an extension, make sure it's installed
if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
throw this._createFatalError(`The ${this.type} "${installSpec}" was not installed, so can't be updated`);
@@ -559,7 +575,8 @@ export abstract class ExtensionCliCommand<ExtType extends ExtensionType = Extens
* successfully loaded and executed for the given extension
* @throws {Error} If any of the mandatory Doctor checks fails.
*/
protected async _doctor({installSpec}: DoctorOptions): Promise<number> {
protected async _doctor({installSpec: rawInstallSpec}: DoctorOptions): Promise<number> {
const installSpec = this._resolveExtensionName(rawInstallSpec);
if (!this.config.isInstalled(installSpec)) {
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
}
@@ -644,11 +661,12 @@ export abstract class ExtensionCliCommand<ExtType extends ExtensionType = Extens
* not found within `scripts` object.
*/
protected async _run({
installSpec,
installSpec: rawInstallSpec,
scriptName,
extraArgs = [],
bufferOutput = false,
}: RunOptions): Promise<RunOutput> {
const installSpec = this._resolveExtensionName(rawInstallSpec);
if (!this.config.isInstalled(installSpec)) {
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
}
+18 -6
View File
@@ -48,6 +48,9 @@ export function configureGlobalFeatures(this: AppiumDriver) {
* @param driverName
*/
export function configureDriverFeatures(this: AppiumDriver, driver: ExternalDriver, driverName: string) {
// the docs tell users to scope a feature with the driver's automationName, which is also what
// the driver itself compares against in `isFeatureEnabled`
const automationName = this.driverConfig?.installedExtensions?.[driverName]?.automationName;
if (this.relaxedSecurityEnabled) {
this.log.info(
`Enabling relaxed security for this session as per the server configuration. ` +
@@ -55,13 +58,13 @@ export function configureDriverFeatures(this: AppiumDriver, driver: ExternalDriv
);
driver.relaxedSecurityEnabled = true;
}
const allowedDriverFeatures = filterInsecureFeatures(this.allowInsecure, driverName);
const allowedDriverFeatures = filterInsecureFeatures(this.allowInsecure, driverName, automationName);
if (!util.isEmpty(allowedDriverFeatures)) {
this.log.info('Explicitly enabling insecure features for this session ' + 'as per the server configuration:');
allowedDriverFeatures.forEach((a) => this.log.info(` ${a}`));
driver.allowInsecure = allowedDriverFeatures;
}
const deniedDriverFeatures = filterInsecureFeatures(this.denyInsecure, driverName);
const deniedDriverFeatures = filterInsecureFeatures(this.denyInsecure, driverName, automationName);
if (util.isEmpty(deniedDriverFeatures)) {
return;
}
@@ -97,20 +100,29 @@ function validateFeatures(features: string[]): string[] {
/**
* Filters the list of insecure features to only those that are
* applicable to the given driver name.
* applicable to the given driver.
* Assumes that all feature names have already been validated
*
* The prefix may be the driver's install name or its automationName, which is the one the
* security guide documents. Both are compared case-insensitively, like the prefix check
* `isFeatureEnabled` does on the driver side.
*
* @param features
* @param driverName
* @param automationName
*/
function filterInsecureFeatures(features: string[], driverName: string = ALL_DRIVERS_MATCH): string[] {
function filterInsecureFeatures(features: string[], driverName?: string, automationName?: string): string[] {
const acceptedPrefixes = new Set(
[ALL_DRIVERS_MATCH, driverName, automationName]
.filter((name): name is string => Boolean(name))
.map((name) => name.toLowerCase()),
);
const filterFn = (fullName: string) => {
const separatorPos = fullName.indexOf(FEATURE_NAME_SEPARATOR);
if (separatorPos <= 0) {
return false;
}
const automationName = fullName.substring(0, separatorPos);
return [driverName, ALL_DRIVERS_MATCH].includes(automationName);
return acceptedPrefixes.has(fullName.substring(0, separatorPos).toLowerCase());
};
return features.filter(filterFn);
}
+18 -3
View File
@@ -1,4 +1,4 @@
import {existsSync, readFileSync} from 'node:fs';
import {readFileSync, statSync} from 'node:fs';
import {ArgumentTypeError} from 'argparse';
@@ -28,7 +28,7 @@ export const transformers = {
let csv = csvOrPath;
let loadedFromFile = false;
// Value could be a single CSV token or a filepath; attempt file first.
if (existsSync(csvOrPath)) {
if (isRegularFile(csvOrPath)) {
try {
csv = readFileSync(csvOrPath, 'utf8');
} catch (err) {
@@ -52,7 +52,7 @@ export const transformers = {
json: (jsonOrPath: string): Record<string, any> => {
let json = jsonOrPath;
let loadedFromFile = false;
if (existsSync(jsonOrPath)) {
if (isRegularFile(jsonOrPath)) {
try {
// Intentionally sync: argparse type hooks are synchronous.
json = readFileSync(jsonOrPath, 'utf8');
@@ -70,6 +70,21 @@ export const transformers = {
},
} as const;
/**
* Whether `filepath` is an existing regular file.
*
* Directories are not files: `--allow-insecure adb_shell` names a feature, not the `./adb_shell`
* folder. Any other stat failure means "not a file" too, since these values are usually not paths
* at all (a long JSON blob fails with ENAMETOOLONG, not ENOENT).
*/
function isRegularFile(filepath: string): boolean {
try {
return statSync(filepath).isFile();
} catch {
return false;
}
}
/**
* Split a file by newline then calls {@link parseCsvLine} on each line.
*/
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "3.7.0",
"version": "3.8.0",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Automation for Apps.",
"keywords": [
@@ -65,28 +65,28 @@
"test:unit": "node --test --enable-source-maps --test-timeout=5000 \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "10.8.0",
"@appium/base-plugin": "3.3.4",
"@appium/docutils": "3.0.0",
"@appium/logger": "2.0.11",
"@appium/base-driver": "10.8.1",
"@appium/base-plugin": "3.3.5",
"@appium/docutils": "3.0.1",
"@appium/logger": "2.0.12",
"@appium/schema": "1.3.0",
"@appium/support": "7.2.7",
"@appium/types": "1.7.0",
"@appium/support": "7.2.8",
"@appium/types": "1.7.1",
"@sidvind/better-ajv-errors": "5.0.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"argparse": "3.0.0",
"asyncbox": "6.4.2",
"axios": "1.19.0",
"argparse": "3.0.2",
"asyncbox": "6.4.3",
"axios": "1.20.0",
"lilconfig": "3.1.3",
"lru-cache": "11.5.2",
"lru-cache": "11.5.3",
"ora": "5.4.1",
"semver": "7.8.5",
"teen_process": "4.2.1",
"type-fest": "5.8.0",
"type-fest": "5.10.0",
"winston": "3.19.0",
"ws": "8.21.3",
"yaml": "2.9.0"
"yaml": "2.9.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "9.31.1"
"webdriverio": "9.31.9"
}
}
@@ -5,6 +5,7 @@ import {describe, it, before, after, beforeEach, type TestContext} from 'node:te
import {fs, system, tempDir, util} from '@appium/support';
import type {DriverType} from '@appium/types';
import type {ExtRecord} from 'appium/types';
import {prerelease} from 'semver';
import {exec} from 'teen_process';
import {
@@ -116,7 +117,9 @@ describe('Driver CLI', {timeout: 90000}, function () {
).stdout,
) as string[];
const penultimateFakeDriverVersionAsOfRightNow = versions[versions.length - 2];
// Update checks skip prereleases, so pick from stable versions to guarantee an update exists.
const stableVersions = versions.filter((v) => !prerelease(v));
const penultimateFakeDriverVersionAsOfRightNow = stableVersions[stableVersions.length - 2];
await resetAppiumHome();
await runInstall([`@appium/fake-driver@${penultimateFakeDriverVersionAsOfRightNow}`, '--source', 'npm']);
+9 -8
View File
@@ -570,14 +570,15 @@ describe('FakeDriver via HTTP', function () {
},
};
let sessionId = null;
const createSessionStub = sandbox
.stub(FakeDriver.prototype, 'createSession')
.callsFake(async function (this: InstanceType<DriverClass>, caps) {
const res = await BaseDriver.prototype.createSession.call(this, caps);
sessionId = res[0];
assert.strictEqual(this.protocol, 'W3C');
return res;
});
const createSessionStub = sandbox.stub(FakeDriver.prototype, 'createSession').callsFake(async function (
this: InstanceType<DriverClass>,
caps,
) {
const res = await BaseDriver.prototype.createSession.call(this, caps);
sessionId = res[0];
assert.strictEqual(this.protocol, 'W3C');
return res;
});
try {
const res = await axios.post(testServerBaseSessionUrl, combinedCaps, {
validateStatus: null,
+25 -2
View File
@@ -309,7 +309,10 @@ describe('AppiumDriver', function () {
describe('configureDriverFeatures', function () {
let appium: InstanceType<typeof AppiumModule.AppiumDriver>;
async function getDriverInstance(appiumArgs: any): Promise<FakeDriver> {
async function getDriverInstance(
appiumArgs: any,
{driverName = 'fake', automationName = 'Fake'} = {},
): Promise<FakeDriver> {
appium = new AppiumDriver(appiumArgs);
appium.configureGlobalFeatures();
const fakeDriver = new FakeDriver();
@@ -320,10 +323,11 @@ describe('AppiumDriver', function () {
// stub does not satisfy DriverConfig typing
(appium as any).driverConfig = {
installedExtensions: {[driverName]: {automationName}},
findMatchingDriver: sandbox.stub().returns({
driver: mockedDriverReturnerClass,
version: '1.2.3',
driverName: 'fake',
driverName,
}),
};
@@ -361,6 +365,25 @@ describe('AppiumDriver', function () {
fakeDriver = await getDriverInstance({allowInsecure: ['fake:foo', 'real:bar']});
assert.deepStrictEqual(fakeDriver.allowInsecure, ['fake:foo']);
});
it(`should apply driver-scope features when the prefix is the automationName`, async function () {
// the docs tell users to prefix with the automationName, which does not have to be
// the name the driver is installed under
fakeDriver = await getDriverInstance(
{allowInsecure: ['MyAutomation:foo', 'real:bar']},
{driverName: 'my-driver', automationName: 'MyAutomation'},
);
assert.deepStrictEqual(fakeDriver.allowInsecure, ['MyAutomation:foo']);
});
it(`should match the feature prefix case-insensitively`, async function () {
fakeDriver = await getDriverInstance({
allowInsecure: ['FAKE:foo'],
denyInsecure: ['Fake:bar'],
});
assert.deepStrictEqual(fakeDriver.allowInsecure, ['FAKE:foo']);
assert.deepStrictEqual(fakeDriver.denyInsecure, ['Fake:bar']);
});
});
describe('getAppiumSessions', function () {
let appium: InstanceType<typeof AppiumModule.AppiumDriver>;
@@ -1,17 +1,22 @@
import assert from 'node:assert/strict';
import type {ChildProcess} from 'node:child_process';
import path from 'node:path';
import type {Writable} from 'node:stream';
import {describe, it, beforeEach, afterEach} from 'node:test';
import {fs, system} from '@appium/support';
import {console, fs, npm, system} from '@appium/support';
import type {AppiumLogger} from '@appium/types';
import type {ExtManifest} from 'appium/types';
import type {SinonSandbox, SinonStub} from 'sinon';
import sinon from 'sinon';
import DriverCliCommand from '../../../lib/cli/driver-command';
import {ExtensionCliCommand, injectAppiumSymlinks} from '../../../lib/cli/extension-command';
import type {ExtensionConfig} from '../../../lib/cli/extension-command';
import PluginCliCommand from '../../../lib/cli/plugin-command';
import {DriverConfig} from '../../../lib/extension/driver-config';
import {Manifest} from '../../../lib/extension/manifest';
import {PluginConfig} from '../../../lib/extension/plugin-config';
import {appiumPackageRoot} from '../../../lib/utils';
import {FAKE_DRIVER_DIR} from '../../helpers';
@@ -82,6 +87,174 @@ describe('ExtensionCommand', function () {
});
});
for (const type of ['driver', 'plugin'] as const) {
describe(`installed ${type} names`, function () {
const commands = ['uninstall', 'update', 'run', 'doctor'] as const;
type Command = (typeof commands)[number];
const pkgName = `appium-test-${type}`;
const scopedPkgName = `@example/${pkgName}`;
const appiumHome = path.resolve('appium-home-for-test');
let ec: ExtensionCliCommand;
let config: DriverConfig | PluginConfig;
let uninstallStub: SinonStub;
let removeStub: SinonStub;
let checkUpdateStub: SinonStub;
let updateStub: SinonStub;
let getInstallPathSpy: sinon.SinonSpy;
let existsStub: SinonStub;
let readFileStub: SinonStub;
function extension(packageName: string): ExtManifest<'driver'> {
return {
pkgName: packageName,
version: '1.0.0',
installType: 'npm',
installSpec: packageName,
installPath: path.join(appiumHome, 'node_modules', packageName),
mainClass: 'TestExtension',
automationName: 'Test',
platformNames: ['Test'],
scripts: {sample: 'scripts/sample.js'},
};
}
function invoke(command: Command, name: string): Promise<unknown> {
return ec.execute({[`${type}Command`]: command, [type]: name, unsafe: false});
}
async function assertHandles(command: Command, input: string, expectedName: string): Promise<void> {
const expected = config.installedExtensions[expectedName];
const result = await invoke(command, input);
switch (command) {
case 'uninstall':
sinon.assert.calledOnceWithExactly(uninstallStub, appiumHome, expected.pkgName);
sinon.assert.calledOnceWithExactly(removeStub, expectedName);
assert.strictEqual(result, config.installedExtensions);
break;
case 'update':
sinon.assert.calledOnceWithExactly(checkUpdateStub, expectedName);
sinon.assert.calledOnceWithExactly(updateStub, expectedName, '1.1.0');
assert.deepStrictEqual(result, {
updates: {[expectedName]: {from: '1.0.0', to: '1.1.0'}},
errors: {},
});
break;
case 'run':
sinon.assert.calledOnceWithExactly(getInstallPathSpy, expectedName);
sinon.assert.calledOnceWithExactly(existsStub, path.join(expected.installPath, 'scripts/sample.js'));
assert.deepStrictEqual(result, {});
break;
case 'doctor':
sinon.assert.calledOnceWithExactly(getInstallPathSpy, expectedName);
sinon.assert.calledOnceWithExactly(readFileStub, path.join(expected.installPath, 'package.json'), 'utf8');
assert.strictEqual(result, 0);
break;
}
}
beforeEach(function () {
sandbox = sinon.createSandbox();
const logger = sandbox.createStubInstance(console.CliConsole);
logger.decorate.callsFake((message) => message);
sandbox.stub(console, 'CliConsole').returns(logger);
const manifest = sandbox.createStubInstance(Manifest);
sandbox.stub(manifest, 'appiumHome').get(() => appiumHome);
if (type === 'driver') {
config = DriverConfig.create(manifest);
ec = new DriverCliCommand({config, json: true});
} else {
config = PluginConfig.create(manifest);
ec = new PluginCliCommand({config, json: true});
}
config.installedExtensions = {short: extension(pkgName), scoped: extension(scopedPkgName)};
uninstallStub = sandbox.stub(npm, 'uninstallPackage').resolves();
removeStub = sandbox.stub(config, 'removeExtension').resolves();
checkUpdateStub = sandbox.stub(ec as any, 'checkForExtensionUpdate').resolves({
current: '1.0.0',
safeUpdate: '1.1.0',
unsafeUpdate: null,
});
updateStub = sandbox.stub(ec as any, 'updateExtension').resolves();
getInstallPathSpy = sandbox.spy(config, 'getInstallPath');
existsStub = sandbox.stub(fs, 'exists').resolves(true);
readFileStub = sandbox.stub(fs, 'readFile').resolves('{"appium":{}}');
});
afterEach(function () {
sandbox.restore();
});
for (const command of commands) {
describe(command, function () {
for (const [input, expectedName] of [
['short', 'short'],
[pkgName, 'short'],
[scopedPkgName, 'scoped'],
]) {
it(`should accept ${input} as the installed extension`, async function () {
await assertHandles(command, input, expectedName);
});
}
it('should prefer an extension name over another extension package name', async function () {
config.installedExtensions[pkgName] = extension('another-package');
await assertHandles(command, pkgName, pkgName);
});
for (const input of ['unknown', pkgName.slice(0, -1), `${pkgName}@1.0.0`, `${scopedPkgName}@1.0.0`]) {
it(`should reject ${input} without changing the installed-extension error`, async function () {
const message =
command === 'uninstall'
? `Can't uninstall ${type} '${input}'; it is not installed`
: command === 'update'
? `The ${type} "${input}" was not installed, so can't be updated`
: `The ${type} "${input}" is not installed`;
await assert.rejects(invoke(command, input), {message});
sinon.assert.notCalled(uninstallStub);
sinon.assert.notCalled(removeStub);
sinon.assert.notCalled(checkUpdateStub);
sinon.assert.notCalled(updateStub);
sinon.assert.notCalled(getInstallPathSpy);
});
}
});
}
it('should update all extensions for installed even when it matches an extension and a package', async function () {
config.installedExtensions.short.pkgName = 'installed';
config.installedExtensions.installed = extension('another-package');
assert.deepStrictEqual(await invoke('update', 'installed'), {
updates: {
short: {from: '1.0.0', to: '1.1.0'},
scoped: {from: '1.0.0', to: '1.1.0'},
installed: {from: '1.0.0', to: '1.1.0'},
},
errors: {},
});
assert.deepStrictEqual(checkUpdateStub.args, [['short'], ['scoped'], ['installed']]);
});
it('should preserve the development extension uninstall restriction for a package name', async function () {
config.installedExtensions.short.installType = 'dev';
assert.strictEqual(await invoke('uninstall', pkgName), config.installedExtensions);
sinon.assert.notCalled(uninstallStub);
sinon.assert.notCalled(removeStub);
});
it('should preserve the non-npm extension update restriction for a package name', async function () {
config.installedExtensions.short.installType = 'local';
const result = (await invoke('update', pkgName)) as {updates: object; errors: Record<string, Error>};
assert.deepStrictEqual(result.updates, {});
assert.deepStrictEqual(Object.keys(result.errors), ['short']);
sinon.assert.notCalled(checkUpdateStub);
sinon.assert.notCalled(updateStub);
});
});
}
describe('injectAppiumSymlinks', function () {
let fsExistsStub: SinonStub;
let fsSymlinkStub: SinonStub;
+23
View File
@@ -1,4 +1,7 @@
import assert from 'node:assert/strict';
import {mkdirSync, mkdtempSync, rmSync} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {describe, it, beforeEach, before, after} from 'node:test';
import {readConfigFile} from '../../lib/bootstrap/config-file';
@@ -95,6 +98,13 @@ describe('parser', function () {
assert.deepStrictEqual(args.defaultCapabilities, defaultCapabilities);
});
it('should parse default capabilities which are longer than the max filename length', function () {
// a value this long fails stat() with ENAMETOOLONG instead of ENOENT on posix systems
const defaultCapabilities = {'appium:app': 'a'.repeat(300)};
const args = p.parseArgs(['--default-capabilities', JSON.stringify(defaultCapabilities)]);
assert.deepStrictEqual(args.defaultCapabilities, defaultCapabilities);
});
it('should parse default capabilities correctly from a file', function () {
const defaultCapabilities = {a: 'b'};
const args = p.parseArgs(['--default-capabilities', CAPS_FIXTURE]);
@@ -143,6 +153,19 @@ describe('parser', function () {
assert.deepStrictEqual(parsed.denyInsecure, ['*:nofeature1', '*:nofeature2', '*:nofeature3']);
});
it('should treat --allow-insecure as a feature name even if a directory with that name exists', function () {
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'appium-allow-insecure-'));
mkdirSync(path.join(tmpDir, 'adb_shell'));
const prevCwd = process.cwd();
try {
process.chdir(tmpDir);
assert.deepStrictEqual(p.parseArgs(['--allow-insecure', 'adb_shell']).allowInsecure, ['adb_shell']);
} finally {
process.chdir(prevCwd);
rmSync(tmpDir, {recursive: true, force: true});
}
});
it('should allow a string for --use-drivers', function () {
assert.deepStrictEqual(p.parseArgs(['--use-drivers', 'fake']).useDrivers, ['fake']);
});
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import {mkdirSync, mkdtempSync, rmSync, writeFileSync} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {after, before, describe, it} from 'node:test';
import {transformers} from '../../../lib/schema/cli-transformers';
describe('cli-transformers', function () {
let tmpDir: string;
let prevCwd: string;
before(function () {
prevCwd = process.cwd();
tmpDir = mkdtempSync(path.join(os.tmpdir(), 'appium-cli-transformers-'));
process.chdir(tmpDir);
});
after(function () {
process.chdir(prevCwd);
rmSync(tmpDir, {recursive: true, force: true});
});
describe('csv', function () {
it('should parse a comma-delimited string', function () {
assert.deepStrictEqual(transformers.csv('adb_shell,get_server_logs'), ['adb_shell', 'get_server_logs']);
});
it('should treat a directory as a literal value instead of reading it', function () {
mkdirSync(path.join(tmpDir, 'adb_shell'));
assert.deepStrictEqual(transformers.csv('adb_shell'), ['adb_shell']);
});
it('should still load a CSV file when the path is a regular file', function () {
const file = path.join(tmpDir, 'features.csv');
writeFileSync(file, 'adb_shell\nget_server_logs\n', 'utf8');
assert.deepStrictEqual(transformers.csv(file), ['adb_shell', 'get_server_logs']);
});
});
describe('json', function () {
it('should parse a JSON string', function () {
assert.deepStrictEqual(transformers.json('{"foo":1}'), {foo: 1});
});
it('should parse a JSON string longer than the max filename length', function () {
// a value this long fails stat() with ENAMETOOLONG instead of ENOENT on posix systems
const caps = {'appium:app': 'a'.repeat(300)};
assert.deepStrictEqual(transformers.json(JSON.stringify(caps)), caps);
});
it('should treat a directory as a literal value instead of reading it', function () {
mkdirSync(path.join(tmpDir, 'caps'));
assert.throws(() => transformers.json('caps'), /must be a valid JSON/i);
});
it('should still load a JSON file when the path is a regular file', function () {
const file = path.join(tmpDir, 'caps.json');
writeFileSync(file, '{"foo":1}', 'utf8');
assert.deepStrictEqual(transformers.json(file), {foo: 1});
});
});
});
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [10.8.1](https://github.com/appium/appium/compare/@appium/base-driver@10.8.0...@appium/base-driver@10.8.1) (2026-09-24)
### Bug Fixes
* **base-driver:** honor unquoted and RFC 5987 Content-Disposition filenames ([#22755](https://github.com/appium/appium/issues/22755)) ([81c8b9d](https://github.com/appium/appium/commit/81c8b9da0aeba9d013a35c39127374477b24cc81))
* **base-driver:** keep existing contexts when session.subscribe is called again ([#22780](https://github.com/appium/appium/issues/22780)) ([3423e36](https://github.com/appium/appium/commit/3423e366897e6a540a4aefc0802e81c74678f51f))
* **base-driver:** reject invalid timeout with InvalidArgumentError ([#22679](https://github.com/appium/appium/issues/22679)) ([9c00b86](https://github.com/appium/appium/commit/9c00b86ce759f84428cfa508f94261042893a6e7))
* **base-driver:** skip unknown events in session.unsubscribe ([#22671](https://github.com/appium/appium/issues/22671)) ([0c4da64](https://github.com/appium/appium/commit/0c4da64aeabb66588241bf5824fa1c42b96b3f3d))
## [10.8.0](https://github.com/appium/appium/compare/@appium/base-driver@10.7.2...@appium/base-driver@10.8.0) (2026-08-24)
### Features
@@ -12,15 +12,18 @@ declare module '../driver' {
const BidiCommands: IBidiCommands = {
async bidiSubscribe<C extends Constraints>(this: BaseDriver<C>, events: string[], contexts: string[] = ['']) {
for (const event of events) {
this.bidiEventSubs[event] = contexts;
// a later subscribe for the same event must keep existing contexts; the spec appends subscriptions
const existing = this.bidiEventSubs[event] ?? [];
this.bidiEventSubs[event] = util.uniq([...existing, ...contexts]);
}
},
async bidiUnsubscribe<C extends Constraints>(this: BaseDriver<C>, events: string[], contexts: string[] = ['']) {
for (const event of events) {
if (this.bidiEventSubs[event]) {
this.bidiEventSubs[event] = this.bidiEventSubs[event].filter((c) => !contexts.includes(c));
if (!this.bidiEventSubs[event]) {
continue;
}
this.bidiEventSubs[event] = this.bidiEventSubs[event].filter((c) => !contexts.includes(c));
if (this.bidiEventSubs[event].length === 0) {
delete this.bidiEventSubs[event];
}
@@ -127,7 +127,7 @@ const TimeoutCommands: ITimeoutCommands = {
parseTimeoutArgument<C extends Constraints>(this: BaseDriver<C>, ms: number | string) {
const duration = parseInt(String(ms), 10);
if (Number.isNaN(duration) || duration < MIN_TIMEOUT) {
throw new errors.UnknownError(`Invalid timeout value '${ms}'`);
throw new errors.InvalidArgumentError(`Invalid timeout value '${ms}'`);
}
return duration;
},
+41 -3
View File
@@ -38,6 +38,13 @@ const APPLICATIONS_CACHE_GUARD = new AsyncLock();
const SANITIZE_REPLACEMENT = '-';
const DEFAULT_BASENAME = 'appium-app';
const APP_DOWNLOAD_TIMEOUT_MS = 120 * 1000;
// RFC 5987: filename*=UTF-8''percent-encoded-name (language tag is optional)
const FILENAME_STAR_PATTERN = /(?:^|;)\s*filename\*\s*=\s*([^';\s]+)'[^']*'([^;]+)/i;
const QUOTED_FILENAME_PATTERN = /(?:^|;)\s*filename\s*=\s*"((?:\\.|[^"\\])*)"/i;
// unquoted token; stops at ';' so later parameters are not swallowed
const UNQUOTED_FILENAME_PATTERN = /(?:^|;)\s*filename\s*=\s*([^;\s]+)/i;
const SURROUNDING_QUOTES_PATTERN = /^["']|["']$/g;
const ESCAPED_CHAR_PATTERN = /\\(.)/g;
process.on('exit', () => {
if (APPLICATIONS_CACHE.size === 0) {
@@ -485,6 +492,37 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
return dstPath;
}
/**
* Picks a download filename from a Content-Disposition header.
* Prefers RFC 5987 `filename*` over `filename`, and accepts both quoted and unquoted `filename`.
* @internal
*/
export function filenameFromContentDisposition(header: string): string | undefined {
const encoded = FILENAME_STAR_PATTERN.exec(header);
if (encoded?.[2]) {
try {
const value = decodeURIComponent(encoded[2].trim().replace(SURROUNDING_QUOTES_PATTERN, ''));
if (value) {
return value;
}
} catch {
// invalid percent-encoding; fall through to filename=
}
}
// an empty quoted value is still the filename parameter, so it must not fall through to the
// token branch below, which would otherwise capture the quote characters themselves
const quoted = QUOTED_FILENAME_PATTERN.exec(header);
if (quoted) {
return quoted[1].replace(ESCAPED_CHAR_PATTERN, '$1') || undefined;
}
const unquoted = UNQUOTED_FILENAME_PATTERN.exec(header);
if (unquoted?.[1]) {
return unquoted[1];
}
}
function determineFilename(
headers: AxiosResponseHeaders | RawAxiosRequestHeaders,
pathname: string,
@@ -496,9 +534,9 @@ function determineFilename(
const extname = path.extname(basename);
if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
if (match) {
return fs.sanitizeName(match[1], {replacement: SANITIZE_REPLACEMENT});
const fromHeader = filenameFromContentDisposition(String(headers['content-disposition']));
if (fromHeader) {
return fs.sanitizeName(fromHeader, {replacement: SANITIZE_REPLACEMENT});
}
}
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "10.8.0",
"version": "10.8.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Base driver class for Appium drivers",
"keywords": [
@@ -46,21 +46,21 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "7.2.7",
"@appium/types": "1.7.0",
"@appium/support": "7.2.8",
"@appium/types": "1.7.1",
"async-lock": "1.4.1",
"asyncbox": "6.4.2",
"axios": "1.19.0",
"asyncbox": "6.4.3",
"axios": "1.20.0",
"body-parser": "2.3.0",
"express": "5.2.1",
"fastest-levenshtein": "1.0.16",
"http-status-codes": "2.3.0",
"lru-cache": "11.5.2",
"lru-cache": "11.5.3",
"method-override": "3.0.0",
"morgan": "1.11.0",
"morgan": "1.12.1",
"path-to-regexp": "8.4.2",
"serve-favicon": "2.5.1",
"type-fest": "5.8.0"
"type-fest": "5.10.0"
},
"optionalDependencies": {
"spdy": "4.0.2"
@@ -66,7 +66,9 @@ describe('app download and configuration', function () {
const serve = serveStatic(FIXTURE_ROOT, {
index: false,
setHeaders: (res, filePath) => {
res.setHeader('Content-Disposition', contentDisposition(filePath));
if (!res.getHeader('Content-Disposition')) {
res.setHeader('Content-Disposition', contentDisposition(filePath));
}
},
});
@@ -76,13 +78,15 @@ describe('app download and configuration', function () {
res.end();
return;
}
// for testing zip file content types
const contentType = new URLSearchParams(new URL(req.url ?? '', 'http://localhost').search).get(
'content-type',
);
const params = new URLSearchParams(new URL(req.url ?? '', 'http://localhost').search);
const contentType = params.get('content-type');
if (contentType !== null) {
res.setHeader('content-type', contentType);
}
const disposition = params.get('disposition');
if (disposition !== null) {
res.setHeader('Content-Disposition', disposition);
}
serve(req, res, finalhandler(req, res));
});
const close = httpServer.close.bind(httpServer);
@@ -130,6 +134,20 @@ describe('app download and configuration', function () {
const contents = await fs.readFile(newAppPath, 'utf8');
assert.strictEqual(contents, 'this is not really an apk\n');
});
it('should use an unquoted Content-Disposition filename', async function () {
const disposition = encodeURIComponent('attachment; filename=from-header.apk');
const newAppPath = await configureApp(`${serverUrl}/FakeAndroidApp.apk?disposition=${disposition}`, '.apk');
assert.ok(newAppPath.includes('from-header.apk'));
const contents = await fs.readFile(newAppPath, 'utf8');
assert.strictEqual(contents, 'this is not really an apk\n');
});
it('should use an RFC 5987 filename* parameter', async function () {
const disposition = encodeURIComponent(`attachment; filename*=UTF-8''from-star.apk`);
const newAppPath = await configureApp(`${serverUrl}/FakeAndroidApp.apk?disposition=${disposition}`, '.apk');
assert.ok(newAppPath.includes('from-star.apk'));
const contents = await fs.readFile(newAppPath, 'utf8');
assert.strictEqual(contents, 'this is not really an apk\n');
});
it('should handle zip file that cannot be downloaded', async function () {
await assert.rejects(configureApp(`${serverUrl}/missing/FakeIOSApp.app.zip`, '.app'));
});
@@ -1,8 +1,7 @@
import {BaseDriver} from '@appium/base-driver';
import {BaseDriverCapConstraints, Driver, DriverOpts, ExternalDriver} from '@appium/types';
import {expectAssignable} from 'tsd';
import {BaseDriver} from '.../../../lib/basedriver/driver';
expectAssignable<Driver<BaseDriverCapConstraints>>(
new BaseDriver<BaseDriverCapConstraints>({} as DriverOpts<BaseDriverCapConstraints>),
);
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import {beforeEach, describe, it} from 'node:test';
import type {InitialOpts} from '@appium/types';
import {BaseDriver} from '../../../../lib';
describe('bidi commands -', function () {
let driver: BaseDriver<any, any, any, any, any, any>;
beforeEach(function () {
driver = new BaseDriver({} as InitialOpts);
});
describe('bidiUnsubscribe', function () {
it('should not throw when the event was never subscribed', async function () {
await driver.bidiUnsubscribe(['log.entryAdded'], ['']);
assert.deepStrictEqual(driver.bidiEventSubs, {});
});
it('should still unsubscribe a matching event when the list also has an unknown one', async function () {
await driver.bidiSubscribe(['log.entryAdded'], ['']);
await driver.bidiUnsubscribe(['log.entryAdded', 'browsingContext.domContentLoaded'], ['']);
assert.deepStrictEqual(driver.bidiEventSubs, {});
});
});
describe('bidiSubscribe', function () {
it('should keep previously subscribed contexts when the same event is subscribed again', async function () {
await driver.bidiSubscribe(['log.entryAdded'], ['ctx-a']);
await driver.bidiSubscribe(['log.entryAdded'], ['ctx-b']);
assert.deepStrictEqual(driver.bidiEventSubs, {
'log.entryAdded': ['ctx-a', 'ctx-b'],
});
});
it('should not duplicate a context that is already subscribed', async function () {
await driver.bidiSubscribe(['log.entryAdded'], ['ctx-a']);
await driver.bidiSubscribe(['log.entryAdded'], ['ctx-a', 'ctx-b']);
assert.deepStrictEqual(driver.bidiEventSubs, {
'log.entryAdded': ['ctx-a', 'ctx-b'],
});
});
it('should not mix contexts across different events', async function () {
await driver.bidiSubscribe(['log.entryAdded'], ['ctx-a']);
await driver.bidiSubscribe(['browsingContext.load'], ['ctx-b']);
assert.deepStrictEqual(driver.bidiEventSubs, {
'log.entryAdded': ['ctx-a'],
'browsingContext.load': ['ctx-b'],
});
});
});
});
@@ -1,7 +1,12 @@
import assert from 'node:assert/strict';
import {describe, it} from 'node:test';
import {duplicateKeys, isPackageOrBundle, parseCapsArray} from '../../../lib/basedriver/helpers';
import {
duplicateKeys,
filenameFromContentDisposition,
isPackageOrBundle,
parseCapsArray,
} from '../../../lib/basedriver/helpers';
describe('helpers', function () {
describe('#isPackageOrBundle', function () {
@@ -118,3 +123,48 @@ describe('parseCapsArray', function () {
assert.throws(() => parseCapsArray(`['*']`));
});
});
describe('filenameFromContentDisposition', function () {
it('should read a quoted filename', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; filename="quoted-app.apk"'), 'quoted-app.apk');
});
it('should read an unquoted filename', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; filename=unquoted-app.apk'), 'unquoted-app.apk');
});
it('should prefer RFC 5987 filename* over filename', function () {
assert.strictEqual(
filenameFromContentDisposition(`attachment; filename="wrong.apk"; filename*=UTF-8''from-star.apk`),
'from-star.apk',
);
});
it('should decode a percent-encoded filename*', function () {
assert.strictEqual(filenameFromContentDisposition(`attachment; filename*=UTF-8''My%20App.apk`), 'My App.apk');
});
it('should not let an unquoted token swallow later parameters', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; filename=app.apk; size=42'), 'app.apk');
});
it('should keep a quoted filename containing a semicolon', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; filename="a;b.apk"'), 'a;b.apk');
});
it('should ignore a parameter which merely ends with filename', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; x-filename=sneaky.apk'), undefined);
});
it('should return undefined for an empty quoted filename', function () {
assert.strictEqual(filenameFromContentDisposition('attachment; filename=""'), undefined);
});
it('should return undefined when filename* is not decodable', function () {
assert.strictEqual(filenameFromContentDisposition(`attachment; filename*=UTF-8''%E0%A4%A`), undefined);
});
it('should return undefined when the header has no filename', function () {
assert.strictEqual(filenameFromContentDisposition('attachment'), undefined);
});
});
@@ -4,7 +4,7 @@ import {after, afterEach, before, beforeEach, describe, it} from 'node:test';
import type {InitialOpts} from '@appium/types';
import {createSandbox} from 'sinon';
import {BaseDriver} from '../../../lib';
import {BaseDriver, errors} from '../../../lib';
describe('timeout', function () {
let driver: BaseDriver<any, any, any, any, any, any>;
@@ -27,6 +27,10 @@ describe('timeout', function () {
describe('timeouts', function () {
describe('errors', function () {
it('should reject an invalid timeout with InvalidArgumentError', function () {
assert.throws(() => driver.parseTimeoutArgument('abc'), errors.InvalidArgumentError);
assert.throws(() => driver.parseTimeoutArgument(-1), errors.InvalidArgumentError);
});
it('should throw an error if something random is sent', async function () {
await assert.rejects(driver.timeouts('random timeout', 'howdy'));
});
+1 -1
View File
@@ -12,7 +12,7 @@
"checkJs": true,
"types": ["node"]
},
"include": ["lib", "test"],
"include": ["lib", "test/unit", "test/e2e", "test/helpers.ts"],
"exclude": ["build"],
"references": [
{
+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.3.5](https://github.com/appium/appium/compare/@appium/base-plugin@3.3.4...@appium/base-plugin@3.3.5) (2026-09-24)
**Note:** Version bump only for package @appium/base-plugin
## [3.3.4](https://github.com/appium/appium/compare/@appium/base-plugin@3.3.3...@appium/base-plugin@3.3.4) (2026-08-24)
**Note:** Version bump only for package @appium/base-plugin
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "3.3.4",
"version": "3.3.5",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "The base plugin used to create Appium plugins",
"keywords": [
@@ -44,9 +44,9 @@
"test:unit": "node --test --enable-source-maps \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "10.8.0",
"@appium/support": "7.2.7",
"@appium/types": "1.7.0"
"@appium/base-driver": "10.8.1",
"@appium/support": "7.2.8",
"@appium/types": "1.7.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+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.1](https://github.com/appium/appium/compare/@appium/docutils@3.0.0...@appium/docutils@3.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/docutils
## [3.0.0](https://github.com/appium/appium/compare/@appium/docutils@2.5.2...@appium/docutils@3.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "3.0.0",
"version": "3.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
@@ -58,14 +58,14 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "7.2.7",
"@appium/support": "7.2.8",
"consola": "3.4.2",
"diff": "9.0.0",
"lilconfig": "3.1.3",
"normalize-package-data": "8.0.0",
"teen_process": "4.2.1",
"type-fest": "5.8.0",
"yaml": "2.9.0",
"type-fest": "5.10.0",
"yaml": "2.9.1",
"yargs": "18.1.0",
"yargs-parser": "22.0.0"
},
+1 -1
View File
@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-git-revision-date-localized-plugin==1.5.4
mkdocs-git-revision-date-localized-plugin==1.6.0
mkdocs-material==9.7.7
mkdocs-redirects==1.2.3
mike==2.2.0
@@ -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.1](https://github.com/appium/appium/compare/@appium/driver-test-support@3.0.0...@appium/driver-test-support@3.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/driver-test-support
## [3.0.0](https://github.com/appium/appium/compare/@appium/driver-test-support@2.0.1...@appium/driver-test-support@3.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "3.0.0",
"version": "3.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Test utilities for Appium drivers",
"keywords": [
@@ -53,7 +53,7 @@
"test:unit": "node --test --enable-source-maps \"./build/test/unit/**/*.spec.js\""
},
"devDependencies": {
"@appium/types": "1.7.0"
"@appium/types": "1.7.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
@@ -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.2.6](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@3.2.5...@appium/eslint-config-appium-ts@3.2.6) (2026-09-24)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
## [3.2.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@3.2.4...@appium/eslint-config-appium-ts@3.2.5) (2026-08-24)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
@@ -1,6 +1,6 @@
{
"name": "@appium/eslint-config-appium-ts",
"version": "3.2.5",
"version": "3.2.6",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
@@ -33,7 +33,7 @@
"test:smoke": "exit 0"
},
"dependencies": {
"@eslint/compat": "2.1.0",
"@eslint/compat": "2.1.1",
"@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0",
"eslint-config-prettier": "10.1.8",
@@ -42,10 +42,10 @@
"eslint-plugin-jsdoc": "63.3.3",
"eslint-plugin-mocha": "11.3.0",
"eslint-plugin-n": "18.3.0",
"eslint-plugin-perfectionist": "5.10.1",
"eslint-plugin-perfectionist": "5.11.1",
"eslint-plugin-promise": "7.3.0",
"eslint-plugin-unicorn": "68.0.0",
"typescript-eslint": "8.67.0"
"typescript-eslint": "8.70.0"
},
"peerDependencies": {
"eslint": "^9.0.0 || ^10.0.0"
@@ -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.
## [7.0.1](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@7.0.0...@appium/execute-driver-plugin@7.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/execute-driver-plugin
## [7.0.0](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@6.0.7...@appium/execute-driver-plugin@7.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/execute-driver-plugin",
"version": "7.0.0",
"version": "7.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Plugin for batching and executing Appium driver commands",
"keywords": [
@@ -49,8 +49,8 @@
"test:e2e": "node --test --enable-source-maps --test-force-exit --test-concurrency=1 --test-timeout=20000 \"./build/test/e2e/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "7.2.7",
"webdriverio": "9.31.1"
"@appium/support": "7.2.8",
"webdriverio": "9.31.9"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0"
+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.
## [6.2.4](https://github.com/appium/appium/compare/@appium/fake-driver@6.2.3...@appium/fake-driver@6.2.4) (2026-09-24)
**Note:** Version bump only for package @appium/fake-driver
## [6.2.3](https://github.com/appium/appium/compare/@appium/fake-driver@6.2.2...@appium/fake-driver@6.2.3) (2026-08-24)
**Note:** Version bump only for package @appium/fake-driver
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-driver",
"version": "6.2.3",
"version": "6.2.4",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Mock driver used internally by Appium for testing. Ignore",
"keywords": [
@@ -47,11 +47,11 @@
},
"dependencies": {
"@xmldom/xmldom": "0.9.12",
"asyncbox": "6.4.2",
"asyncbox": "6.4.3",
"xpath": "0.0.34"
},
"devDependencies": {
"@appium/driver-test-support": "3.0.0"
"@appium/driver-test-support": "3.0.1"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0"
+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.
## [4.4.1](https://github.com/appium/appium/compare/@appium/fake-plugin@4.4.0...@appium/fake-plugin@4.4.1) (2026-09-24)
**Note:** Version bump only for package @appium/fake-plugin
## [4.4.0](https://github.com/appium/appium/compare/@appium/fake-plugin@4.3.3...@appium/fake-plugin@4.4.0) (2026-08-24)
### Features
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-plugin",
"version": "4.4.0",
"version": "4.4.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "A fake Appium plugin",
"keywords": [
@@ -43,12 +43,12 @@
"test:unit": "node --test --enable-source-maps --test-timeout=5000 \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-plugin": "3.3.4",
"@appium/support": "7.2.7",
"asyncbox": "6.4.2"
"@appium/base-plugin": "3.3.5",
"@appium/support": "7.2.8",
"asyncbox": "6.4.3"
},
"devDependencies": {
"@appium/types": "1.7.0",
"@appium/types": "1.7.1",
"express": "5.2.1"
},
"peerDependencies": {
+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.
## [5.0.1](https://github.com/appium/appium/compare/@appium/images-plugin@5.0.0...@appium/images-plugin@5.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/images-plugin
## [5.0.0](https://github.com/appium/appium/compare/@appium/images-plugin@4.2.8...@appium/images-plugin@5.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/images-plugin",
"version": "5.0.0",
"version": "5.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Plugin for working with images and image elements in Appium",
"keywords": [
@@ -46,14 +46,14 @@
"scripts": {
"test": "npm run test:unit",
"test:smoke": "node ./build/lib/index.js",
"test:unit": "node --test --experimental-test-module-mocks --enable-source-maps --test-timeout=5000 \"./build/test/unit/**/*.spec.js\"",
"test:unit": "node --test --experimental-test-module-mocks --enable-source-maps --test-timeout=10000 \"./build/test/unit/**/*.spec.js\"",
"test:e2e": "node --test --enable-source-maps --test-force-exit --test-concurrency=1 --test-timeout=20000 \"./build/test/e2e/**/*.spec.js\""
},
"dependencies": {
"@appium/opencv": "5.0.0",
"@appium/support": "7.2.7",
"lru-cache": "11.5.2",
"sharp": "0.35.3"
"@appium/opencv": "5.0.1",
"@appium/support": "7.2.8",
"lru-cache": "11.5.3",
"sharp": "0.35.4"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0"
@@ -175,7 +175,9 @@ describe('finding elements by image', function () {
assert.deepStrictEqual(await f.findByImage(template, d as any, {multiple: true}), []);
});
it('should respect implicit wait', async function () {
(d as any).setImplicitWait(10);
// the wait budget must exceed the 500ms poll interval, otherwise whether the retry happens
// at all depends on how quickly the first attempt returns
(d as any).setImplicitWait(2000);
compareStub.resetHistory();
compareStub.returns({rect, score});
compareStub.onFirstCall().throws(new Error('Cannot find any occurrences'));
+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.
## [2.0.12](https://github.com/appium/appium/compare/@appium/logger@2.0.11...@appium/logger@2.0.12) (2026-09-24)
**Note:** Version bump only for package @appium/logger
## [2.0.11](https://github.com/appium/appium/compare/@appium/logger@2.0.10...@appium/logger@2.0.11) (2026-08-24)
### Bug Fixes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/logger",
"version": "2.0.11",
"version": "2.0.12",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "A Universal Logger For The Appium Ecosystem",
"keywords": [
@@ -44,7 +44,7 @@
"test:unit": "node --test --enable-source-maps --test-timeout=5000 \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"lru-cache": "11.5.2"
"lru-cache": "11.5.3"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+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.
## [5.0.1](https://github.com/appium/appium/compare/@appium/opencv@5.0.0...@appium/opencv@5.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/opencv
## [5.0.0](https://github.com/appium/appium/compare/@appium/opencv@4.1.7...@appium/opencv@5.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/opencv",
"version": "5.0.0",
"version": "5.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "OpenCV-related helper library",
"keywords": [
@@ -53,7 +53,7 @@
},
"dependencies": {
"opencv-bindings": "4.5.5",
"sharp": "0.35.3"
"sharp": "0.35.4"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+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.
## [1.1.2](https://github.com/appium/appium/compare/@appium/oxc-config@1.1.1...@appium/oxc-config@1.1.2) (2026-09-24)
**Note:** Version bump only for package @appium/oxc-config
## [1.1.1](https://github.com/appium/appium/compare/@appium/oxc-config@1.1.0...@appium/oxc-config@1.1.1) (2026-08-24)
**Note:** Version bump only for package @appium/oxc-config
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/oxc-config",
"version": "1.1.1",
"version": "1.1.2",
"description": "Shared Oxlint and Oxfmt config for Appium projects",
"keywords": [
"appium",
@@ -36,9 +36,9 @@
"test:smoke": "node scripts/test-smoke.mjs"
},
"dependencies": {
"oxfmt": "0.64.0",
"oxlint": "1.79.0",
"oxlint-tsgolint": "7.0.2001"
"oxfmt": "0.68.0",
"oxlint": "1.83.0",
"oxlint-tsgolint": "7.0.2002"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
@@ -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.1](https://github.com/appium/appium/compare/@appium/plugin-test-support@3.0.0...@appium/plugin-test-support@3.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/plugin-test-support
## [3.0.0](https://github.com/appium/appium/compare/@appium/plugin-test-support@2.0.1...@appium/plugin-test-support@3.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/plugin-test-support",
"version": "3.0.0",
"version": "3.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Testing utilities for Appium plugins",
"keywords": [
@@ -55,7 +55,7 @@
"teen_process": "4.2.1"
},
"devDependencies": {
"@appium/types": "1.7.0"
"@appium/types": "1.7.1"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0"
@@ -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.1](https://github.com/appium/appium/compare/@appium/relaxed-caps-plugin@3.0.0...@appium/relaxed-caps-plugin@3.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/relaxed-caps-plugin
## [3.0.0](https://github.com/appium/appium/compare/@appium/relaxed-caps-plugin@2.2.5...@appium/relaxed-caps-plugin@3.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/relaxed-caps-plugin",
"version": "3.0.0",
"version": "3.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "An Appium plugin that loosens requirements for vendor prefixes on caps",
"keywords": [
@@ -52,7 +52,7 @@
},
"dependencies": {},
"devDependencies": {
"@appium/types": "1.7.0"
"@appium/types": "1.7.1"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0"
+7
View File
@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.1](https://github.com/appium/appium/compare/@appium/storage-plugin@2.0.0...@appium/storage-plugin@2.0.1) (2026-09-24)
### Bug Fixes
* **storage-plugin:** map StorageArgumentError to InvalidArgumentError ([#22734](https://github.com/appium/appium/issues/22734)) ([f522b64](https://github.com/appium/appium/commit/f522b64456509d078a81c2d214cd99f9a3090205))
## [2.0.0](https://github.com/appium/appium/compare/@appium/storage-plugin@1.1.8...@appium/storage-plugin@2.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+2 -1
View File
@@ -5,6 +5,7 @@ import type Stream from 'node:stream';
import {fs, timing, util} from '@appium/support';
import type {AppiumLogger} from '@appium/types';
import {errors} from 'appium/driver.js';
import AsyncLock from 'async-lock';
import {asyncmap} from 'asyncbox';
import type {Path} from 'path-scurry';
@@ -218,7 +219,7 @@ export class Storage {
}
}
export class StorageArgumentError extends Error {}
export class StorageArgumentError extends errors.InvalidArgumentError {}
/**
* Validates storage item options and returns the same object when valid.
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/storage-plugin",
"version": "2.0.0",
"version": "2.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Server-side storage plugin for Appium",
"keywords": [
@@ -52,10 +52,10 @@
"test:e2e": "node --test --enable-source-maps --test-force-exit --test-concurrency=1 --test-timeout=20000 \"./build/test/e2e/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "7.2.7",
"@appium/support": "7.2.8",
"async-lock": "1.4.1",
"asyncbox": "6.4.2",
"lru-cache": "11.5.2",
"asyncbox": "6.4.3",
"lru-cache": "11.5.3",
"ws": "8.21.3"
},
"peerDependencies": {
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import {describe, it} from 'node:test';
import type {AppiumServer} from '@appium/types';
import type {Express, Request, Response} from 'express';
import {StoragePlugin} from '../../lib/plugin.js';
interface CapturedResponse {
status: number;
body: any;
}
/**
* Registers the plugin routes against a stub app and returns a function which invokes the
* handler bound to `routePath` with the given request body.
*/
async function routeCaller(routePath: string): Promise<(body: unknown) => Promise<CapturedResponse>> {
const routes: Record<string, (req: Request, res: Response) => Promise<void>> = {};
const register = (path: string, handler: (req: Request, res: Response) => Promise<void>) => {
routes[path] = handler;
};
const app = {post: register, get: register} as unknown as Express;
// the request under test is rejected before the websocket setup runs, so this stub is never used
await StoragePlugin.updateServer(app, {} as unknown as AppiumServer);
const handler = routes[routePath];
assert.ok(handler, `No handler was registered for ${routePath}`);
return async function callRoute(body: unknown): Promise<CapturedResponse> {
const captured: CapturedResponse = {status: 0, body: undefined};
const res = {
set: () => res,
status: (code: number) => {
captured.status = code;
return res;
},
send: (payload: any) => {
captured.body = payload;
return res;
},
} as unknown as Response;
await handler({body} as Request, res);
return captured;
};
}
describe('StoragePlugin routes', function () {
it('should reject an invalid add name as an invalid argument', async function () {
const callRoute = await routeCaller('/appium/storage/add');
const {status, body} = await callRoute({
name: 'foo/bar',
sha1: 'ccc963411b2621335657963322890305ebe96186',
});
assert.strictEqual(status, 400);
assert.strictEqual(body.value.error, 'invalid argument');
});
});
@@ -3,6 +3,7 @@ import path from 'node:path';
import {after, afterEach, before, beforeEach, describe, it} from 'node:test';
import {fs, logger, tempDir} from '@appium/support';
import {errors, getResponseForW3CError} from 'appium/driver.js';
import {Storage, StorageArgumentError, validateStorageItemName} from '../../lib/storage.js';
@@ -139,6 +140,18 @@ describe('storage', function () {
},
);
});
it('should be reported as a W3C invalid argument', function () {
try {
validateStorageItemName('foo/bar');
assert.fail('expected validateStorageItemName to throw');
} catch (err) {
assert.ok(err instanceof errors.InvalidArgumentError);
const [status, body] = getResponseForW3CError(err);
assert.strictEqual(status, 400);
assert.strictEqual(body.value.error, 'invalid argument');
}
});
});
async function addFileToStorage(name: string, size: number): Promise<string> {
+7
View File
@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.1](https://github.com/appium/appium/compare/@appium/strongbox@2.0.0...@appium/strongbox@2.0.1) (2026-09-24)
### Bug Fixes
* **strongbox:** reject item names that slugify to an empty path ([#22717](https://github.com/appium/appium/issues/22717)) ([afa4837](https://github.com/appium/appium/commit/afa4837530a814e3ddf3b901574cf2b12e1e3d11))
## [2.0.0](https://github.com/appium/appium/compare/@appium/strongbox@1.1.3...@appium/strongbox@2.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+6 -1
View File
@@ -223,6 +223,10 @@ export class Strongbox<Options extends StrongboxOpts = StrongboxOpts> implements
encoding = encodingOrCtor;
encodingOrCtor = this.defaultItemCtor;
}
if (!slugify(name)) {
// the slug is the filename; an empty one would resolve to the container directory itself
throw new TypeError(`Item name '${name}' must contain at least one alphanumeric character`);
}
const item = new (encodingOrCtor ?? (this.defaultItemCtor as ItemCtor<T>))(name, this, encoding);
if (this.getLiveItem(item.id)) {
throw new ReferenceError(`Item with id "${item.id}" already exists`);
@@ -388,7 +392,8 @@ export class Strongbox<Options extends StrongboxOpts = StrongboxOpts> implements
throw e;
}
for await (const ent of dir) {
if (ent.isFile()) {
// a basename with no alphanumerics has no slug, so it cannot belong to an item
if (ent.isFile() && slugify(ent.name)) {
yield ent.name;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/strongbox",
"version": "2.0.0",
"version": "2.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Persistent storage for Appium extensions",
"keywords": [
@@ -193,6 +193,17 @@ describe('Strongbox', function () {
assert.strictEqual(item.encoding, 'base64');
});
});
describe('when the name has no alphanumeric characters', function () {
it('should reject instead of pointing the item at the container', async function () {
for (const name of ['---', '@#$', ' ', '']) {
await assert.rejects(box.createItem(name), {
name: 'TypeError',
message: `Item name '${name}' must contain at least one alphanumeric character`,
});
}
});
});
});
describe('clearAll()', function () {
@@ -283,6 +294,15 @@ describe('Strongbox', function () {
assert.strictEqual(MockFs.opendir.calledWith(box.container), true);
});
it('should skip files whose name has no alphanumeric characters', async function () {
mockOpendir([dirent('good'), dirent('_')]);
const items = await box.listItems();
assert.deepStrictEqual(
items.map((i) => i.name),
['good'],
);
});
it('should return an empty array when the container does not exist', async function () {
const err = Object.assign(new Error('ENOENT'), {code: 'ENOENT'});
MockFs.opendir.rejects(err);
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.2.8](https://github.com/appium/appium/compare/@appium/support@7.2.7...@appium/support@7.2.8) (2026-09-24)
### Bug Fixes
* **support:** avoid TOCTOU race in getLockFileGuard() acquisition ([#22807](https://github.com/appium/appium/issues/22807)) ([7df378f](https://github.com/appium/appium/commit/7df378f4010c63d01b526876f60c72694b5fa0da))
* **support:** do not delete a path when fs.mv destination is the source or inside it ([#22806](https://github.com/appium/appium/issues/22806)) ([4dd2e2d](https://github.com/appium/appium/commit/4dd2e2d47dbae909f6e69202a9b5f4aa4576bef1))
* **support:** do not set file-sized Content-Length on default multipart uploads ([#22766](https://github.com/appium/appium/issues/22766)) ([9a70a44](https://github.com/appium/appium/commit/9a70a44c83734fef535a1e651320828ddc7c1846))
* **support:** extract zip entries whose names start with dots ([#22772](https://github.com/appium/appium/issues/22772)) ([dc16d69](https://github.com/appium/appium/commit/dc16d6970f9806d46f92c41510bdeec4bee07702))
## [7.2.7](https://github.com/appium/appium/compare/@appium/support@7.2.6...@appium/support@7.2.7) (2026-08-24)
### Bug Fixes
+8 -1
View File
@@ -30,7 +30,7 @@ import {type NormalizedPackageJson, type NormalizeOptions, packageDirectorySync,
import log from './logger';
import {isWindows} from './system';
import {Timer} from './timing';
import {memoize, pluralize} from './util';
import {isSameDestination, isSubPath, memoize, pluralize} from './util';
const findRootCached = memoize(packageDirectorySync, (opts: {cwd?: string} | undefined) => opts?.cwd);
@@ -253,6 +253,13 @@ export const fs = {
}
throw err;
}
// moving a path onto itself deletes it, and a destination inside the source never returns
if (await isSameDestination(from, to)) {
return;
}
if (fromStat.isDirectory() && isSubPath(path.resolve(to), path.resolve(from))) {
throw new Error(`Cannot move '${from}' to '${to}' because the destination is inside the source`);
}
if (fromStat.isFile()) {
const dstRootWasCreated = await ensureDestination(path.dirname(to));
await renameFile(from, to, dstRootWasCreated);
+7 -2
View File
@@ -9,6 +9,7 @@ import {Timer} from './timing';
import {isPlainObject, toReadableSizeString} from './util';
const DEFAULT_TIMEOUT_MS = 4 * 60 * 1000;
const DEFAULT_FILE_FIELD_NAME = 'file';
/** Common options for {@linkcode uploadFile} and {@linkcode downloadFile}. */
export interface NetOptions {
@@ -108,7 +109,11 @@ export async function uploadFile(
}
const timer = new Timer().start();
if (isHttpUploadOptions(uploadOptions, url)) {
if (!uploadOptions.fileFieldName) {
// mirror the default applied in uploadFileToHttp: only an absent name becomes the multipart
// default, so an explicitly falsy one still means a raw body that needs Content-Length
const fileFieldName =
uploadOptions.fileFieldName === undefined ? DEFAULT_FILE_FIELD_NAME : uploadOptions.fileFieldName;
if (!fileFieldName) {
uploadOptions.headers = {
...(isPlainObject(uploadOptions.headers) ? uploadOptions.headers : {}),
'Content-Length': size,
@@ -224,7 +229,7 @@ async function uploadFileToHttp(
timeout = DEFAULT_TIMEOUT_MS,
headers,
auth,
fileFieldName = 'file',
fileFieldName = DEFAULT_FILE_FIELD_NAME,
formFields,
} = uploadOptions;
const {href} = parsedUri;
+9 -3
View File
@@ -653,10 +653,16 @@ export function getLockFileGuard<T>(lockFile: string, opts: LockFileOptions = {}
let acquired = false;
while (!acquired) {
try {
if (_lockfile.checkSync(lockFile)) {
await lock(lockFile, {wait: timeout * 1000});
} else {
try {
_lockfile.lockSync(lockFile);
} catch (e) {
const lockErr = e as NodeJS.ErrnoException;
if (lockErr.code !== 'EEXIST') {
throw lockErr;
}
// Someone else is holding the lock (possibly grabbed it between us checking and
// locking) -- wait for them to release it instead of failing outright.
await lock(lockFile, {wait: timeout * 1000});
}
acquired = true;
} catch (e) {
+3 -9
View File
@@ -15,7 +15,7 @@ import {createBase64EncodeStream} from './internal';
import log from './logger';
import {isWindows} from './system';
import {Timer} from './timing';
import {GiB, memoize, toReadableSizeString} from './util';
import {GiB, isSubPath, memoize, toReadableSizeString} from './util';
const openZip = promisify(yauzl.open) as (zipPath: string, options?: yauzl.Options) => Promise<yauzl.ZipFile>;
@@ -650,14 +650,8 @@ const getExecutablePath = memoize(
);
function isContainedPath(originalPath: string, root: string): boolean {
for (const p of [originalPath, root]) {
if (!path.isAbsolute(p)) {
throw new Error(`'${p}' is expected to be an absolute path`);
}
}
const relativePath = path.relative(root, originalPath);
return !relativePath || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
// `startsWith('..')` would reject a safe name like `..foo`; isSubPath only treats `..` as a parent
return isSubPath(originalPath, root);
}
export default {
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/support",
"version": "7.2.7",
"version": "7.2.8",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Support libs used across Appium packages",
"keywords": [
@@ -44,14 +44,14 @@
"test:unit": "node --test --enable-source-maps \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/logger": "2.0.11",
"@appium/types": "1.7.0",
"@appium/logger": "2.0.12",
"@appium/types": "1.7.1",
"archiver": "8.0.0",
"asyncbox": "6.4.2",
"axios": "1.19.0",
"asyncbox": "6.4.3",
"axios": "1.20.0",
"bluebird": "3.7.2",
"bplist-creator": "0.1.1",
"bplist-parser": "0.3.2",
"bplist-creator": "0.3.0",
"bplist-parser": "0.5.0",
"form-data": "4.0.6",
"glob": "13.0.6",
"jsftp": "2.1.3",
@@ -64,7 +64,7 @@
"semver": "7.8.5",
"shell-quote": "1.10.0",
"teen_process": "4.2.1",
"type-fest": "5.8.0",
"type-fest": "5.10.0",
"uuid": "14.0.2",
"which": "6.0.1",
"yauzl": "3.4.0"
+20
View File
@@ -58,6 +58,26 @@ describe('fs', function () {
assert.strictEqual((await fs.readFile(dstPath)).toString(), 'foo');
});
it('should keep a file when the source and destination are the same path', async function () {
const srcPath = path.join(srcRoot!, 'src.file');
await fs.writeFile(srcPath, Buffer.from('bar'));
await fs.mv(srcPath, path.join(srcRoot!, '.', 'src.file'));
assert.strictEqual(await fs.exists(srcPath), true);
assert.strictEqual((await fs.readFile(srcPath)).toString(), 'bar');
});
it('should reject moving a directory into itself', async function () {
const srcPath = path.join(srcRoot!, 'foo', 'src.file');
await fs.mkdirp(path.dirname(srcPath));
await fs.writeFile(srcPath, Buffer.from('bar'));
await assert.rejects(
fs.mv(path.dirname(srcPath), path.join(path.dirname(srcPath), 'child'), {mkdirp: true}),
/inside the source/,
);
assert.strictEqual(await fs.exists(srcPath), true);
assert.strictEqual((await fs.readFile(srcPath)).toString(), 'bar');
});
it('should override a file if already exists by default', async function () {
const srcPath = path.join(srcRoot!, 'src.file');
await fs.writeFile(srcPath, Buffer.from('bar'));
+14
View File
@@ -86,6 +86,20 @@ describe('#zip', function () {
assert.strictEqual(await fs.exists(path.resolve(escapePath, 'owned.txt')), false);
},
);
it('should extract an entry whose name starts with dots but stays in the destination', async function () {
const outputPath = path.resolve(tmpRoot, 'output-dotdot');
const dstPath = path.resolve(tmpRoot, 'dotdot-name.zip');
await createStoredZip(dstPath, [
{
name: '..foo.txt',
contents: 'safe\n',
mode: 0o100644,
},
]);
await zip.extractAllTo(dstPath, outputPath, options);
assert.strictEqual(await fs.readFile(path.resolve(outputPath, '..foo.txt'), {encoding: 'utf8'}), 'safe\n');
});
});
describe('assertValidZip', function () {
+99 -1
View File
@@ -1,8 +1,51 @@
import assert from 'node:assert/strict';
import {describe, it} from 'node:test';
import http from 'node:http';
import type {AddressInfo} from 'node:net';
import path from 'node:path';
import {after, before, describe, it} from 'node:test';
import {fs, tempDir} from '../../lib';
import {uploadFile} from '../../lib/net';
const FILE_BYTES = 100;
async function receiveUpload(run: (url: string) => Promise<void>): Promise<{
headerLength: number | undefined;
received: number;
contentType: string | undefined;
}> {
let headerLength: string | undefined;
let received = 0;
let contentType: string | undefined;
const server = http.createServer((req, res) => {
headerLength = req.headers['content-length'];
contentType = req.headers['content-type'];
req.on('data', (chunk) => {
received += chunk.length;
});
req.on('end', () => {
res.writeHead(200);
res.end('ok');
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => resolve());
});
const {port} = server.address() as AddressInfo;
try {
await run(`http://127.0.0.1:${port}/upload`);
} finally {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
}
return {
headerLength: headerLength === undefined ? undefined : Number(headerLength),
received,
contentType,
};
}
describe('net', function () {
describe('uploadFile()', function () {
it('should accept remote URLs typed as strings', function () {
@@ -14,5 +57,60 @@ describe('net', function () {
assert.strictEqual(typeof upload, 'function');
});
describe('HTTP upload', function () {
let tmpDir: string;
let localPath: string;
before(async function () {
tmpDir = await tempDir.openDir();
localPath = path.join(tmpDir, 'payload.bin');
await fs.writeFile(localPath, Buffer.alloc(FILE_BYTES, 0x61));
});
after(async function () {
await fs.rimraf(tmpDir);
});
it('should send a complete multipart body when fileFieldName is omitted', async function () {
const {headerLength, received, contentType} = await receiveUpload((url) =>
uploadFile(localPath, url, {isMetered: false}),
);
assert.match(String(contentType), /multipart\/form-data/i);
assert.ok(received > FILE_BYTES);
assert.strictEqual(headerLength, received);
});
it('should send a complete multipart body when fileFieldName is set', async function () {
const {headerLength, received, contentType} = await receiveUpload((url) =>
uploadFile(localPath, url, {isMetered: false, fileFieldName: 'file'}),
);
assert.match(String(contentType), /multipart\/form-data/i);
assert.ok(received > FILE_BYTES);
assert.strictEqual(headerLength, received);
});
// any falsy fileFieldName is documented as a raw upload, so it must carry Content-Length
it('should send a raw body with Content-Length when fileFieldName is null', async function () {
const {headerLength, received, contentType} = await receiveUpload((url) =>
uploadFile(localPath, url, {
isMetered: false,
fileFieldName: null as unknown as string,
}),
);
assert.doesNotMatch(String(contentType ?? ''), /multipart\/form-data/i);
assert.strictEqual(received, FILE_BYTES);
assert.strictEqual(headerLength, FILE_BYTES);
});
it('should send a raw body when fileFieldName is empty', async function () {
const {headerLength, received, contentType} = await receiveUpload((url) =>
uploadFile(localPath, url, {isMetered: false, fileFieldName: ''}),
);
assert.doesNotMatch(String(contentType ?? ''), /multipart\/form-data/i);
assert.strictEqual(received, FILE_BYTES);
assert.strictEqual(headerLength, FILE_BYTES);
});
});
});
});
+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.
## [1.7.1](https://github.com/appium/appium/compare/@appium/types@1.7.0...@appium/types@1.7.1) (2026-09-24)
**Note:** Version bump only for package @appium/types
## [1.7.0](https://github.com/appium/appium/compare/@appium/types@1.6.0...@appium/types@1.7.0) (2026-08-24)
### Features
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/types",
"version": "1.7.0",
"version": "1.7.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Various type declarations used across Appium",
"keywords": [
@@ -42,10 +42,10 @@
},
"dependencies": {
"@appium/schema": "1.3.0",
"type-fest": "5.8.0"
"type-fest": "5.10.0"
},
"devDependencies": {
"@appium/logger": "2.0.11",
"@appium/logger": "2.0.12",
"@appium/tsconfig": "1.2.1"
},
"peerDependencies": {
@@ -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.1](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@3.0.0...@appium/universal-xml-plugin@3.0.1) (2026-09-24)
**Note:** Version bump only for package @appium/universal-xml-plugin
## [3.0.0](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@2.1.7...@appium/universal-xml-plugin@3.0.0) (2026-08-24)
### ⚠ BREAKING CHANGES
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/universal-xml-plugin",
"version": "3.0.0",
"version": "3.0.1",
"gitHead": "f7b20335eab4022e5cbbb627ec86866a994444f8",
"description": "Appium plugin for making XML source and XPath queries the same across iOS and Android",
"keywords": [
@@ -48,9 +48,9 @@
"test:unit": "node --test --enable-source-maps \"./build/test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "7.2.7",
"@appium/support": "7.2.8",
"@xmldom/xmldom": "0.9.12",
"fast-xml-parser": "5.11.0",
"fast-xml-parser": "5.11.1",
"xpath": "0.0.34"
},
"peerDependencies": {