Compare commits

...
Author SHA1 Message Date
github-actions 9c35776756 chore: publish
- appium@3.4.0
 - @appium/base-driver@10.5.0
 - @appium/base-plugin@3.2.2
 - @appium/docutils@2.4.0
 - @appium/driver-test-support@1.2.2
 - @appium/eslint-config-appium-ts@3.2.0
 - @appium/execute-driver-plugin@6.0.3
 - @appium/fake-driver@6.1.3
 - @appium/fake-plugin@4.2.2
 - @appium/images-plugin@4.2.2
 - @appium/opencv@4.1.4
 - @appium/plugin-test-support@1.2.2
 - @appium/relaxed-caps-plugin@2.2.2
 - @appium/schema@1.1.1
 - @appium/storage-plugin@1.1.2
 - @appium/support@7.2.0
 - @appium/test-support@4.1.2
 - @appium/types@1.4.0
 - @appium/universal-xml-plugin@2.1.3
2026-05-06 19:01:52 +00:00
Mykola Mokhnach 1b31db3be9 ci: Add a workflow for executing lerna version (#22249) 2026-05-06 20:59:36 +02:00
renovate[bot] 0a4b4f5a55 chore(support): update dependency glob to v13.0.6 (#22244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 17:42:13 +02:00
Mykola Mokhnach a085d44616 ci: Apply scoped release recovery (#22248) 2026-05-06 17:41:59 +02:00
Mykola Mokhnach a10f558382 ci: tune recovery logic (#22247) 2026-05-06 17:19:23 +02:00
Mykola Mokhnach f61e7d57fa ci: Release recovery script (#22246) 2026-05-06 17:09:23 +02:00
renovate[bot] 705710adc5 chore(support): update dependency axios to v1.16.0 (#22240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 14:50:17 +00:00
renovate[bot] 6a3d659c5a chore(support): update dependency teen_process to v4.1.3 (#22241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 13:46:54 +00:00
dependabot[bot] c760f02e14 chore(deps): bump ip-address (#22243)
Bumps the npm_and_yarn group with 1 update in the / directory: [ip-address](https://github.com/beaugunderson/ip-address).


Updates `ip-address` from 9.0.5 to 10.2.0
- [Commits](https://github.com/beaugunderson/ip-address/commits)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.2.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-05-06 12:27:52 +03:00
renovate[bot] e1ab96fc4e chore(docutils): update dependency yaml to v2.8.4 (#22239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 08:47:02 +00:00
github-actions[bot]andmykola-mokhnach e8d31c63c4 docs(appium): Update documentation translations (#22238)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2026-05-06 09:20:11 +03:00
Edgars Eglītis 787571d338 feat: add 3 WebDriver extension endpoints (#22237)
* feat: add 2 WebDriver extension endpoints

* docs: add missing reference for removeAuthCredential

* fix protocol hash
2026-05-05 20:30:59 +03:00
Edgars Eglītis 4849ad015d feat(eslint-config-appium-ts): support ESLint 10 (#22236) 2026-05-05 11:15:02 +03:00
renovate[bot] 0878f3c8ce chore(workflows): update fuxingloh/multi-labeler action to v5 (#22235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 09:05:43 +03:00
renovate[bot] 3dd0ac715a chore(support): update dependency asyncbox to v6.2.0 (#22233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 20:27:40 +00:00
Edgars Eglītis ce09eb2e7d chore: fix new linting errors in ESLint 10 (#22230)
* fix no-useless-assignment errors

* fix preserve-caught-error errors

* address comments

* address comments
2026-05-04 20:44:23 +03:00
renovate[bot] 9cc0377acf chore(execute-driver-plugin): update dependency webdriverio to v9.27.1 (#22232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 15:15:08 +00:00
renovate[bot] 9b21b92418 chore(workflows): update slackapi/slack-github-action action to v3.0.3 (#22229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-02 13:35:08 +00:00
renovate[bot] c758fcaf13 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.59.1 (#22228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-02 09:06:07 +00:00
Mykola Mokhnach 63218cdac6 feat(support): Replace lodash with native alternatives (#22225) 2026-05-01 23:17:40 +02:00
Edgars Eglītis 08493df3d1 feat(docutils): add mermaid diagram support (#22227) 2026-05-01 19:47:22 +03:00
Mykola Mokhnach bde5bd57f4 feat(eslint-config-appium-ts): Add rule to warn if a type is not imported properly (#22226) 2026-05-01 17:47:26 +02:00
renovate[bot] b5bdfdd4e4 chore(eslint-config-appium-ts): update dependency eslint-plugin-promise to v7.3.0 (#22222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 14:43:29 +00:00
Edgars Eglītis cb4aa687a2 chore(deps): update plist to v4 (#22224) 2026-04-30 13:52:34 +03:00
renovate[bot] 180f2611c1 chore(support): update dependency plist to v3.1.1 (#22221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 10:06:28 +00:00
Mykola Mokhnach 1f3119ec1f chore(support): Deprecate v1, v3 and v5 uuid generators (#22218) 2026-04-30 07:16:26 +02:00
Mykola Mokhnach 2c0fb7a2e4 feat(support): Add various helper utils (#22213) 2026-04-29 20:29:19 +02:00
Sai Asish Y fa1e87535b feat(base-driver): map W3C 'no such shadow root' to NoSuchShadowRootError (#22210)
Appium's base-driver had no dedicated ProtocolError subclass for
the W3C WebDriver 'no such shadow root' error. Every such response
from an underlying browser (the GetShadowRoot command against an
element whose shadow DOM is absent) fell through the
w3cErrorCodeMap lookup to UnknownError, making client-side error
handling (Selenium's NoSuchShadowRootException in particular)
unreachable.

Add NoSuchShadowRootError with error='no such shadow root' /
w3cStatus=404 / code=65, register it in the errors map so
errorFromW3CJsonCode returns it, and extend the existing
errors.spec table-tests to lock the new mapping.

Fixes #22209

Signed-off-by: SAY-5 <say.apm35@gmail.com>
2026-04-29 19:19:50 +02:00
renovate[bot] 79370cd307 chore(appium): update dependency ajv to v8.20.0 (#22216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 20:05:03 +02:00
renovate[bot] 712832ae61 chore(support): update dependency axios to v1.15.2 (#22205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 17:00:15 +00:00
renovate[bot] 6c9f7bbf6a chore(universal-xml-plugin): update dependency fast-xml-parser to v5.7.2 (#22215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 11:44:36 +00:00
renovate[bot] 0f0f60effd chore(support): update dependency teen_process to v4.1.2 (#22212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-25 13:43:11 +02:00
renovate[bot] be35e0b3d1 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.59.0 (#22208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 15:14:49 +00:00
100 changed files with 1617 additions and 733 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
name: Auto-Label PRs
runs-on: ubuntu-latest
steps:
- uses: fuxingloh/multi-labeler@v4
- uses: fuxingloh/multi-labeler@v5
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
CROWDIN_PROJECT_ID: ${{ vars[matrix.project_id_var] }}
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
- name: Post to Slack for ${{ matrix.project_name }}
uses: slackapi/slack-github-action@v3.0.2
uses: slackapi/slack-github-action@v3.0.3
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
# GITHUB_TOKEN is optional - if not provided, uses unauthenticated requests (rate limited)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post to Slack
uses: slackapi/slack-github-action@v3.0.2
uses: slackapi/slack-github-action@v3.0.3
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
+84 -1
View File
@@ -50,6 +50,89 @@ jobs:
printf "n\n" | npx lerna version --exact --no-git-tag-version --no-push
else
echo "Running lerna publish for real"
npm run release -- --yes
set +e
release_output="$(npm run release -- --yes 2>&1)"
release_exit_code=$?
set -e
printf '%s\n' "$release_output"
if [ $release_exit_code -eq 0 ]; then
exit 0
fi
if [[ "$release_output" == *"fatal: tag '"*"' already exists"* ]]; then
existing_tag="$(printf '%s\n' "$release_output" | sed -n "s/.*fatal: tag '\\([^']*\\)' already exists.*/\\1/p" | head -n 1)"
if [ -z "$existing_tag" ]; then
echo "Detected existing tag failure, but could not parse tag name"
exit $release_exit_code
fi
echo "Detected existing tag failure for ${existing_tag}; attempting publish recovery from tagged commit"
git fetch --tags --force origin
tagged_commit="$(git rev-list -n 1 "$existing_tag")"
if [ -z "$tagged_commit" ]; then
echo "Tag ${existing_tag} was not found after fetching tags"
exit $release_exit_code
fi
git checkout "$tagged_commit"
missing=()
for pkg_json in packages/*/package.json; do
if [ ! -f "$pkg_json" ]; then
continue
fi
if [ "$(jq -r '.private // false' "$pkg_json")" = "true" ]; then
continue
fi
pkg_name="$(jq -r '.name // empty' "$pkg_json")"
pkg_version="$(jq -r '.version // empty' "$pkg_json")"
if [ -z "$pkg_name" ] || [ -z "$pkg_version" ]; then
echo "Skipping malformed package manifest: $pkg_json"
continue
fi
pkg_spec="${pkg_name}@${pkg_version}"
if ! npm view "$pkg_spec" version >/dev/null 2>&1; then
missing+=("$pkg_spec")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "Missing package versions on npm before scoped recovery:"
printf ' - %s\n' "${missing[@]}"
echo "Publishing missing packages one-by-one with scope"
for pkg_spec in "${missing[@]}"; do
pkg_name="${pkg_spec%@*}"
echo "Publishing ${pkg_name}"
set +e
npx lerna publish from-package --yes --scope "$pkg_name"
scoped_exit_code=$?
set -e
if [ $scoped_exit_code -ne 0 ]; then
echo "Scoped retry failed for ${pkg_name}"
fi
done
final_missing=()
for pkg_spec in "${missing[@]}"; do
if ! npm view "$pkg_spec" version >/dev/null 2>&1; then
final_missing+=("$pkg_spec")
fi
done
if [ ${#final_missing[@]} -gt 0 ]; then
echo "Recovery incomplete after scoped retries. Still missing:"
printf ' - %s\n' "${final_missing[@]}"
exit $release_exit_code
fi
fi
echo "All publishable workspace package versions are present on npm; treating recovery as successful."
else
echo "Release failed and no known recovery condition matched"
exit $release_exit_code
fi
fi
+38
View File
@@ -0,0 +1,38 @@
name: Appium Server Version
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
jobs:
version:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && contains(fromJSON('["mykola-mokhnach", "jlipps", "eglitise", "KazuCocoa"]'), github.actor)
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure git user
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
- name: Setup Node and install deps (LTS)
uses: ./.github/actions/setup-build
with:
node-version: lts/*
install-command: npm ci
run-build: 'true'
- name: Run Lerna version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx lerna version --exact --yes
+383 -485
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -85,7 +85,7 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/js": "9.39.4",
"@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0",
"@tsconfig/node20": "20.1.9",
"@types/archiver": "7.0.0",
@@ -116,19 +116,19 @@
"@types/ws": "8.18.1",
"@types/xmldom": "0.1.34",
"@types/yargs": "17.0.35",
"asyncbox": "6.1.0",
"asyncbox": "6.2.0",
"chai": "6.2.2",
"chai-as-promised": "8.0.2",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "7.0.0",
"cross-env": "10.1.0",
"eslint": "9.39.4",
"eslint": "10.3.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-mocha": "11.2.0",
"eslint-plugin-n": "17.24.0",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-promise": "7.3.0",
"finalhandler": "2.1.1",
"get-port": "7.2.0",
"json-schema-to-typescript": "15.0.4",
@@ -146,12 +146,12 @@
"sinon": "21.1.2",
"sinon-chai": "4.0.1",
"sync-monorepo-packages": "1.0.2",
"teen_process": "4.1.1",
"teen_process": "4.1.3",
"ts-node": "10.9.2",
"tsd": "0.33.0",
"typescript": "6.0.3",
"typescript-eslint": "8.58.2",
"webdriverio": "9.27.0",
"typescript-eslint": "8.59.1",
"webdriverio": "9.27.1",
"ws": "8.20.0"
},
"engines": {
@@ -163,10 +163,10 @@
},
"overrides": {
"@sidvind/better-ajv-errors": {
"ajv": "8.18.0"
"ajv": "8.20.0"
},
"ajv-formats": {
"ajv": "8.18.0"
"ajv": "8.20.0"
},
"http-cache-semantics": "4.2.0"
}
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.4.0](https://github.com/appium/appium/compare/appium@3.3.1...appium@3.4.0) (2026-05-06)
### Features
* add 3 WebDriver extension endpoints ([#22237](https://github.com/appium/appium/issues/22237)) ([787571d](https://github.com/appium/appium/commit/787571d33836c06bb1332d7bc6cc2f9800bbdd82))
## [3.3.1](https://github.com/appium/appium/compare/appium@3.3.0...appium@3.3.1) (2026-04-23)
@@ -396,6 +396,45 @@ Deletes the virtual sensor with the type identified by `:sensorType`.
`null`
## Global Privacy Control Protocol
The [Global Privacy Control protocol](https://www.w3.org/TR/gpc) (GPC) is an extension of the W3C
WebDriver protocol.
### setGlobalPrivacyControl
```
POST /session/:sessionId/privacy
```
> GPC documentation: [Set Global Privacy Control](https://www.w3.org/TR/gpc/#set-global-privacy-control)
Sets the value of the do-not-sell-or-share preference for the current session.
#### Parameters
|Name|Description|Type|
|--|--|--|
|`gpc`|Whether the browser should perform do-not-sell-or-share interactions|boolean|
#### Response
`null`
### getGlobalPrivacyControl
```
GET /session/:sessionId/privacy
```
> GPC documentation: [Get Global Privacy Control](https://www.w3.org/TR/gpc/#get-global-privacy-control)
Retrieves the value of the do-not-sell-or-share preference for the current session.
#### Response
`boolean` - the preference value
## Permissions Protocol
The [Permissions protocol](https://www.w3.org/TR/permissions/) is an extension of the W3C WebDriver
@@ -523,6 +562,32 @@ Retrieves the available log types that can be used to call the [`getLog`](#getlo
`string[]` - an array of log types
## Storage Access Protocol
The [Storage Access protocol](https://privacycg.github.io/storage-access/) is an extension of the
W3C WebDriver protocol.
### setStorageAccess
```
POST /session/:sessionId/storageaccess
```
> Storage Access documentation: [Set Storage Access](https://privacycg.github.io/storage-access/#set-storage-access-command)
Sets the storage access policy of the current browsing context.
#### Parameters
|Name|Description|Type|
|--|--|--|
|`blocked`|Whether storage access should be blocked (`true`) or allowed (`false`)|boolean|
|`origin`|The origin to which the policy should be applied. Must be either a valid URL, or an asterisk (`*`) if the policy should apply to all origins.|string|
#### Response
`null`
## Web Authentication Protocol
The [Web Authentication protocol](https://w3c.github.io/webauthn/) (WebAuthn) is an extension of
@@ -397,6 +397,45 @@ Deletes the virtual sensor with the type identified by `:sensorType`.
`null`
## Global Privacy Control Protocol
The [Global Privacy Control protocol](https://www.w3.org/TR/gpc) (GPC) is an extension of the W3C
WebDriver protocol.
### setGlobalPrivacyControl
```
POST /session/:sessionId/privacy
```
> GPC documentation: [Set Global Privacy Control](https://www.w3.org/TR/gpc/#set-global-privacy-control)
Sets the value of the do-not-sell-or-share preference for the current session.
#### Parameters
| Name | Description | Type |
| ----- | -------------------------------------------------------------------- | ------- |
| `gpc` | Whether the browser should perform do-not-sell-or-share interactions | boolean |
#### Response
`null`
### getGlobalPrivacyControl
```
GET /session/:sessionId/privacy
```
> GPC documentation: [Get Global Privacy Control](https://www.w3.org/TR/gpc/#get-global-privacy-control)
Retrieves the value of the do-not-sell-or-share preference for the current session.
#### Response
`boolean` - the preference value
## Permissions Protocol
The [Permissions protocol](https://www.w3.org/TR/permissions/) is an extension of the W3C WebDriver
@@ -524,6 +563,32 @@ Retrieves the available log types that can be used to call the [`getLog`](#getlo
`string[]` - an array of log types
## Storage Access Protocol
The [Storage Access protocol](https://privacycg.github.io/storage-access/) is an extension of the
W3C WebDriver protocol.
### setStorageAccess
```
POST /session/:sessionId/storageaccess
```
> Storage Access documentation: [Set Storage Access](https://privacycg.github.io/storage-access/#set-storage-access-command)
Sets the storage access policy of the current browsing context.
#### Parameters
| Name | Description | Type |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `blocked` | Whether storage access should be blocked (`true`) or allowed (`false`) | boolean |
| `origin` | The origin to which the policy should be applied. Must be either a valid URL, or an asterisk (`*`) if the policy should apply to all origins. | string |
#### Response
`null`
## Web Authentication Protocol
The [Web Authentication protocol](https://w3c.github.io/webauthn/) (WebAuthn) is an extension of
@@ -397,6 +397,45 @@ Deletes the virtual sensor with the type identified by `:sensorType`.
`null`
## Global Privacy Control Protocol
The [Global Privacy Control protocol](https://www.w3.org/TR/gpc) (GPC) is an extension of the W3C
WebDriver protocol.
### setGlobalPrivacyControl
```
POST /session/:sessionId/privacy
```
> GPC documentation: [Set Global Privacy Control](https://www.w3.org/TR/gpc/#set-global-privacy-control)
Sets the value of the do-not-sell-or-share preference for the current session.
#### Parameters
| Name | Description | Type |
| ----- | -------------------------------------------------------------------- | ------- |
| `gpc` | Whether the browser should perform do-not-sell-or-share interactions | boolean |
#### Response
`null`
### getGlobalPrivacyControl
```
GET /session/:sessionId/privacy
```
> GPC documentation: [Get Global Privacy Control](https://www.w3.org/TR/gpc/#get-global-privacy-control)
Retrieves the value of the do-not-sell-or-share preference for the current session.
#### Response
`boolean` - the preference value
## Permissions Protocol
The [Permissions protocol](https://www.w3.org/TR/permissions/) is an extension of the W3C WebDriver
@@ -524,6 +563,32 @@ Retrieves the available log types that can be used to call the [`getLog`](#getlo
`string[]` - an array of log types
## Storage Access Protocol
The [Storage Access protocol](https://privacycg.github.io/storage-access/) is an extension of the
W3C WebDriver protocol.
### setStorageAccess
```
POST /session/:sessionId/storageaccess
```
> Storage Access documentation: [Set Storage Access](https://privacycg.github.io/storage-access/#set-storage-access-command)
Sets the storage access policy of the current browsing context.
#### Parameters
| Name | Description | Type |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `blocked` | Whether storage access should be blocked (`true`) or allowed (`false`) | boolean |
| `origin` | The origin to which the policy should be applied. Must be either a valid URL, or an asterisk (`*`) if the policy should apply to all origins. | string |
#### Response
`null`
## Web Authentication Protocol
The [Web Authentication protocol](https://w3c.github.io/webauthn/) (WebAuthn) is an extension of
+2 -1
View File
@@ -256,10 +256,11 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
if (bidiProxyUrl) {
try {
new URL(bidiProxyUrl);
} catch {
} catch (e) {
throw new Error(
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
`url ${bidiProxyUrl}, but this was not a valid url`,
{cause: e}
);
}
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}. ` +
+4 -2
View File
@@ -129,7 +129,8 @@ function yamlLoader(filepath: string, content: string): unknown {
return yaml.parse(content);
} catch (e) {
throw new Error(
`The YAML config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`
`The YAML config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`,
{cause: e}
);
}
}
@@ -145,7 +146,8 @@ function jsonLoader(filepath: string, content: string): unknown {
} catch (e) {
rawConfig.delete(filepath);
throw new Error(
`The JSON config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`
`The JSON config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`,
{cause: e}
);
}
}
@@ -147,7 +147,10 @@ export async function requireDir(
return;
} catch {}
}
throw new Error(`The ${displayName} '${root}' must exist and be a valid directory`);
throw new Error(
`The ${displayName} '${root}' must exist and be a valid directory`,
{cause: e}
);
}
if (stat && !stat.isDirectory()) {
throw new Error(`The ${displayName} '${root}' must be a valid directory`);
@@ -155,9 +158,10 @@ export async function requireDir(
if (requireWriteable) {
try {
await fs.access(root, fs.constants.W_OK);
} catch {
} catch (e) {
throw new Error(
`The ${displayName} '${root}' must be writeable for the current user account '${os.userInfo().username}'`
`The ${displayName} '${root}' must be writeable for the current user account '${os.userInfo().username}'`,
{cause: e}
);
}
}
@@ -83,7 +83,7 @@ export class DriverConfig extends ExtensionConfig<DriverType> {
`Have you installed a driver that supports those ` +
`capabilities? Run 'appium driver list --installed' to see. ` +
`(Lower-level error: ${err.message})`;
throw new Error(msg);
throw new Error(msg, {cause: err});
}
}
@@ -591,7 +591,8 @@ export abstract class ExtensionConfig<ExtType extends ExtensionType> {
extensionManifest = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
} catch (e: any) {
throw new ReferenceError(
`Could not read the ${this.extensionType} manifest at ${packageJsonPath}: ${e.message}`
`Could not read the ${this.extensionType} manifest at ${packageJsonPath}: ${e.message}`,
{cause: e}
);
}
let entryPointRelativePath: string | undefined;
@@ -602,7 +603,8 @@ export abstract class ExtensionConfig<ExtType extends ExtensionType> {
entryPointRelativePath = entryPointRelativePath ?? extensionManifest.main ?? DEFAULT_ENTRY_POINT;
} catch (e: any) {
throw new ReferenceError(
`Could not find the ${this.extensionType} installed at ${moduleRoot}: ${e.message}`
`Could not find the ${this.extensionType} installed at ${moduleRoot}: ${e.message}`,
{cause: e}
);
}
const entryPointFullPath = path.resolve(moduleRoot, entryPointRelativePath as string);
+6 -3
View File
@@ -118,7 +118,8 @@ export class Manifest {
`Appium had trouble loading the extension installation ` +
`cache file (${manifestPathResolved}). It may be invalid YAML. Specific error: ${
err.message
}`
}`,
{cause: err}
);
}
}
@@ -166,7 +167,8 @@ export class Manifest {
throw new Error(
`Appium could not create the directory for the manifest file: ${path.dirname(
manifestPathResolved
)}. Original error: ${err.message}`
)}. Original error: ${err.message}`,
{cause: err}
);
}
try {
@@ -176,7 +178,8 @@ export class Manifest {
throw new Error(
`Appium could not write to manifest at ${manifestPathResolved} using APPIUM_HOME ${
this.#appiumHome
}. Please ensure it is writable. Original error: ${err.message}`
}. Please ensure it is writable. Original error: ${err.message}`,
{cause: err}
);
}
})();
@@ -20,7 +20,8 @@ export async function packageDidChange(appiumHome: string): Promise<boolean> {
await fs.mkdirp(hashFilenameDir);
} catch (err: any) {
throw new Error(
`Appium could not create the directory for hash file: ${hashFilenameDir}. Original error: ${err.message}`
`Appium could not create the directory for hash file: ${hashFilenameDir}. Original error: ${err.message}`,
{cause: err}
);
}
@@ -46,7 +47,8 @@ export async function packageDidChange(appiumHome: string): Promise<boolean> {
);
} catch (err: any) {
throw new Error(
`Appium could not write hash file: ${hashFilenameDir}. Original error: ${err.message}`
`Appium could not write hash file: ${hashFilenameDir}. Original error: ${err.message}`,
{cause: err}
);
}
}
@@ -41,7 +41,7 @@ export const transformers = {
const msg = loadedFromFile
? `The provided value of '${csvOrPath}' must be a valid CSV`
: `Must be a comma-delimited string, e.g., "foo,bar,baz"`;
throw new TypeError(`${msg}. Original error: ${(err as Error).message}`);
throw new TypeError(`${msg}. Original error: ${(err as Error).message}`, {cause: err});
}
},
@@ -66,7 +66,7 @@ export const transformers = {
const msg = loadedFromFile
? `'${jsonOrPath}' must be a valid JSON`
: `The provided value must be a valid JSON`;
throw new TypeError(`${msg}. Original error: ${(e as Error).message}`);
throw new TypeError(`${msg}. Original error: ${(e as Error).message}`, {cause: e});
}
},
} as const;
+4 -7
View File
@@ -21,19 +21,16 @@ export const getAppiumModuleRoot = _.memoize(function getAppiumModuleRoot(): str
*/
export function adler32(str: string, seed: number | null = null): number {
let a = 1,
b = 0,
M = 0,
c = 0,
d = 0;
b = 0;
const L = str.length;
if (typeof seed === 'number') {
a = seed & 0xffff;
b = seed >>> 16;
}
for (let i = 0; i < L;) {
M = Math.min(L - i, 2918);
let M = Math.min(L - i, 2918);
while (M > 0) {
c = str.charCodeAt(i++);
let c = str.charCodeAt(i++);
if (c < 0x80) {
a += c;
} else if (c < 0x800) {
@@ -43,7 +40,7 @@ export function adler32(str: string, seed: number | null = null): number {
a += 128 | (c & 63);
} else if (c >= 0xd800 && c < 0xe000) {
c = (c & 1023) + 64;
d = str.charCodeAt(i++) & 1023;
const d = str.charCodeAt(i++) & 1023;
a += 240 | ((c >> 8) & 7);
b += a;
--M;
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "3.3.1",
"version": "3.4.0",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,18 +60,18 @@
"test:unit": "mocha \"./test/unit/**/*.spec.ts\""
},
"dependencies": {
"@appium/base-driver": "10.4.0",
"@appium/base-plugin": "3.2.1",
"@appium/docutils": "2.3.1",
"@appium/base-driver": "10.5.0",
"@appium/base-plugin": "3.2.2",
"@appium/docutils": "2.4.0",
"@appium/logger": "2.0.7",
"@appium/schema": "1.1.0",
"@appium/support": "7.1.1",
"@appium/types": "1.3.1",
"@appium/schema": "1.1.1",
"@appium/support": "7.2.0",
"@appium/types": "1.4.0",
"@sidvind/better-ajv-errors": "5.0.0",
"ajv": "8.18.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"argparse": "2.0.1",
"axios": "1.15.0",
"axios": "1.16.0",
"bluebird": "3.7.2",
"lilconfig": "3.1.3",
"lodash": "4.18.1",
@@ -80,11 +80,11 @@
"package-changed": "3.0.0",
"resolve-from": "5.0.0",
"semver": "7.7.4",
"teen_process": "4.1.1",
"teen_process": "4.1.3",
"type-fest": "5.6.0",
"winston": "3.19.0",
"ws": "8.20.0",
"yaml": "2.8.3"
"yaml": "2.8.4"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "9.27.0"
"webdriverio": "9.27.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.5.0](https://github.com/appium/appium/compare/@appium/base-driver@10.4.0...@appium/base-driver@10.5.0) (2026-05-06)
### Features
* add 3 WebDriver extension endpoints ([#22237](https://github.com/appium/appium/issues/22237)) ([787571d](https://github.com/appium/appium/commit/787571d33836c06bb1332d7bc6cc2f9800bbdd82))
* **base-driver:** map W3C 'no such shadow root' to NoSuchShadowRootError ([#22210](https://github.com/appium/appium/issues/22210)) ([fa1e875](https://github.com/appium/appium/commit/fa1e87535be36647983b70362f00533ef7843b40)), closes [#22209](https://github.com/appium/appium/issues/22209)
## [10.4.0](https://github.com/appium/appium/compare/@appium/base-driver@10.3.0...@appium/base-driver@10.4.0) (2026-04-23)
@@ -130,7 +130,6 @@ export class BaseDriver<
unexpectedShutdownRejecter = null;
// @ts-ignore typescript cannot understand this
unexpectedShutdownResolver?.();
unexpectedShutdownResolver = null;
}
// if we have set a new command timeout (which is the default), start a
@@ -354,7 +354,7 @@ export function parseCapsArray(capValue: string | string[]): string[] {
} catch (e) {
const message = `Failed to parse capability as JSON array: ${(e as Error).message}`;
if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
throw new TypeError(message);
throw new TypeError(message, {cause: e});
}
logger.warn(message);
}
@@ -453,7 +453,10 @@ async function queryAppLink(appLink: string, reqHeaders: RawAxiosRequestHeaders)
const {data: stream, headers, status} = await axios(requestOpts);
return {stream, headers, status};
} catch (err) {
throw new Error(`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`);
throw new Error(
`Cannot download the app from ${axiosUrl}: ${(err as Error).message}`,
{cause: err}
);
}
}
@@ -472,7 +475,7 @@ async function fetchApp(srcStream: Readable, dstPath: string): Promise<string> {
});
});
} catch (err) {
throw new Error(`Cannot fetch the application: ${(err as Error).message}`);
throw new Error(`Cannot fetch the application: ${(err as Error).message}`, {cause: err});
}
const secondsElapsed = timer.getDuration().asSeconds;
@@ -209,7 +209,8 @@ export class JWProxy {
logger.markSensitive(truncateBody(body))
);
throw new Error(
'Cannot interpret the request body as valid JSON. Check the server log for more details.'
'Cannot interpret the request body as valid JSON. Check the server log for more details.',
{cause: error}
);
}
} else {
@@ -140,6 +140,39 @@ export class NoSuchElementError extends ProtocolError {
}
/**
* W3C WebDriver "no such shadow root" error. Issued when a command
* targets an element's shadow root that does not exist (or has been
* detached). Previously this was mis-classified as UnknownError — see
* https://github.com/appium/appium/issues/22209.
*/
export class NoSuchShadowRootError extends ProtocolError {
constructor(message: string = '', cause?: Error) {
super(
message ||
'The element does not have a shadow root attached.',
NoSuchShadowRootError.code(),
NoSuchShadowRootError.w3cStatus(),
NoSuchShadowRootError.error(),
cause,
);
}
// W3C-only error. No historical JSONWP code is assigned to
// "no such shadow root"; keep a placeholder so the super call
// typechecks (other errors in this file follow the same shape).
static code() {
return 65;
}
static error() {
return 'no such shadow root';
}
static w3cStatus() {
return HTTPStatusCodes.NOT_FOUND;
}
}
export class NoSuchFrameError extends ProtocolError {
constructor(message: string = '', cause?: Error) {
super(
@@ -1047,6 +1080,7 @@ export const errors = {
NoSuchContextError,
InvalidContextError,
NoSuchFrameError,
NoSuchShadowRootError,
UnableToCaptureScreen,
UnknownMethodError,
UnsupportedOperationError,
@@ -620,7 +620,7 @@ async function doJwpProxy(driver: BaseDriver<any>, req: Request, res: Response):
if (isErrorType(err, errors.ProxyRequestError)) {
throw err;
} else {
throw new Error(`Could not proxy. Proxy error: ${err.message}`);
throw new Error(`Could not proxy. Proxy error: ${err.message}`, {cause: err});
}
}
}
+12 -1
View File
@@ -564,6 +564,17 @@ export const METHOD_MAP = {
POST: {command: 'updateVirtualPressureSource', payloadParams: {required: ['sample']}},
DELETE: {command: 'deleteVirtualPressureSource'},
},
// Global Privacy Control (GPC)
// https://www.w3.org/TR/gpc/
'/session/:sessionId/privacy': {
GET: {command: 'getGlobalPrivacyControl'},
POST: {command: 'setGlobalPrivacyControl', payloadParams: {required: ['gpc']}},
},
// Storage Access
// https://privacycg.github.io/storage-access/
'/session/:sessionId/storageaccess': {
POST: {command: 'setStorageAccess', payloadParams: {required: ['blocked', 'origin']}},
},
// #endregion
} as const satisfies MethodMap<Driver>;
@@ -594,7 +605,7 @@ export function routeToCommandName(
normalizedPathname = new URL(`https://appium.io${normalizedEndpoint}`).pathname;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`'${endpoint}' cannot be translated to a command name: ${msg}`);
throw new Error(`'${endpoint}' cannot be translated to a command name: ${msg}`, {cause: err});
}
const normalizedMethod = _.toUpper(method ?? '');
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "10.4.0",
"version": "10.5.0",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -45,12 +45,12 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "7.1.1",
"@appium/types": "1.3.1",
"@appium/support": "7.2.0",
"@appium/types": "1.4.0",
"@colors/colors": "1.6.0",
"async-lock": "1.4.1",
"asyncbox": "6.1.0",
"axios": "1.15.0",
"asyncbox": "6.2.0",
"axios": "1.16.0",
"bluebird": "3.7.2",
"body-parser": "2.2.2",
"express": "5.2.1",
@@ -24,6 +24,7 @@ const errorsList: ErrorListItem[] = [
{errorName: 'InvalidArgumentError', errorMsg: 'The arguments passed to the command are either invalid or malformed', error: 'invalid argument'},
{errorName: 'NoSuchElementError', errorMsg: 'An element could not be located on the page using the given search parameters.', error: 'no such element', errorCode: 7},
{errorName: 'NoSuchFrameError', errorMsg: 'A request to switch to a frame could not be satisfied because the frame could not be found.', error: 'no such frame', errorCode: 8},
{errorName: 'NoSuchShadowRootError', errorMsg: 'The element does not have a shadow root attached.', error: 'no such shadow root', errorCode: 65},
{errorName: 'UnknownCommandError', errorMsg: 'The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.', error: 'unknown command', errorCode: 9},
{errorName: 'StaleElementReferenceError', errorMsg: 'An element command failed because the referenced element is no longer attached to the DOM.', error: 'stale element reference', errorCode: 10},
{errorName: 'ElementNotVisibleError', errorMsg: 'An element command could not be completed because the element is not visible on the page.', errorCode: 11},
@@ -120,6 +121,7 @@ describe('w3c Status Codes', function () {
const non400Errors: [string, number][] = [
['NoSuchDriverError', 404],
['NoSuchFrameError', 404],
['NoSuchShadowRootError', 404],
['NoAlertOpenError', 404],
['NoSuchWindowError', 404],
['StaleElementReferenceError', 404],
@@ -38,7 +38,7 @@ describe('Routes', function () {
}
}
const hash = shasum.digest('hex').substring(0, 8);
expect(hash).to.equal('510cbe79');
expect(hash).to.equal('60cf896b');
});
});
+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.2.2](https://github.com/appium/appium/compare/@appium/base-plugin@3.2.1...@appium/base-plugin@3.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/base-plugin
## [3.2.1](https://github.com/appium/appium/compare/@appium/base-plugin@3.2.0...@appium/base-plugin@3.2.1) (2026-04-23)
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "3.2.1",
"version": "3.2.2",
"description": "The base plugin used to create Appium plugins",
"keywords": [
"automation",
@@ -40,9 +40,9 @@
"test:unit": "mocha \"./test/unit/**/*.spec.*ts\""
},
"dependencies": {
"@appium/base-driver": "10.4.0",
"@appium/support": "7.1.1",
"@appium/types": "1.3.1"
"@appium/base-driver": "10.5.0",
"@appium/support": "7.2.0",
"@appium/types": "1.4.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.4.0](https://github.com/appium/appium/compare/@appium/docutils@2.3.1...@appium/docutils@2.4.0) (2026-05-06)
### Features
* **docutils:** add mermaid diagram support ([#22227](https://github.com/appium/appium/issues/22227)) ([08493df](https://github.com/appium/appium/commit/08493df3d12c78c1234c1efdc993c58e42d0abdb))
## [2.3.1](https://github.com/appium/appium/compare/@appium/docutils@2.3.0...@appium/docutils@2.3.1) (2026-04-23)
+5 -1
View File
@@ -18,7 +18,11 @@ markdown_extensions:
- pymdownx.keys
- pymdownx.mark
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
+5 -2
View File
@@ -220,8 +220,11 @@ export class DocutilsValidator extends EventEmitter {
requiredPackages.push({name, version});
}
log.debug('Parsed %s: %O', NAME_REQUIREMENTS_TXT, requiredPackages);
} catch {
throw new DocutilsError(`Could not find ${REQUIREMENTS_TXT_PATH}. This is a bug`);
} catch (e) {
throw new DocutilsError(
`Could not find ${REQUIREMENTS_TXT_PATH}. This is a bug`,
{cause: e}
);
}
return (this.requirementsTxt = requiredPackages);
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "2.3.1",
"version": "2.4.0",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -46,16 +46,16 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "7.1.1",
"@appium/support": "7.2.0",
"consola": "3.4.2",
"diff": "9.0.0",
"lilconfig": "3.1.3",
"lodash": "4.18.1",
"package-directory": "8.2.0",
"read-pkg": "10.1.0",
"teen_process": "4.1.1",
"teen_process": "4.1.3",
"type-fest": "5.6.0",
"yaml": "2.8.3",
"yaml": "2.8.4",
"yargs": "18.0.0",
"yargs-parser": "22.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.
## [1.2.2](https://github.com/appium/appium/compare/@appium/driver-test-support@1.2.1...@appium/driver-test-support@1.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/driver-test-support
## [1.2.1](https://github.com/appium/appium/compare/@appium/driver-test-support@1.2.0...@appium/driver-test-support@1.2.1) (2026-04-23)
@@ -264,12 +264,12 @@ export function driverUnitTestSuite<C extends Constraints>(
for (let i = 0; i < numCmds; i++) {
cmds.push(d.executeCommand('getStatus'));
}
let results = await Promise.all(cmds) as number[];
await Promise.all(cmds) as number[];
cmds = [];
for (let i = 0; i < numCmds; i++) {
cmds.push(d.executeCommand('getStatus'));
}
results = await Promise.all(cmds) as number[];
const results = await Promise.all(cmds) as number[];
for (let i = 1; i < numCmds; i++) {
if (results[i] <= results[i - 1]) {
throw new Error('Got result out of order');
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "1.2.1",
"version": "1.2.2",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -39,8 +39,8 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"asyncbox": "6.1.0",
"axios": "1.15.0",
"asyncbox": "6.2.0",
"axios": "1.16.0",
"chai": "6.2.2",
"chai-as-promised": "8.0.2",
"get-port": "7.2.0",
@@ -49,7 +49,7 @@
"type-fest": "5.6.0"
},
"devDependencies": {
"@appium/types": "1.3.1"
"@appium/types": "1.4.0"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0",
@@ -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.
## [3.2.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@3.1.0...@appium/eslint-config-appium-ts@3.2.0) (2026-05-06)
### Features
* **eslint-config-appium-ts:** Add rule to warn if a type is not imported properly ([#22226](https://github.com/appium/appium/issues/22226)) ([bde5bd5](https://github.com/appium/appium/commit/bde5bd57f4b64bacf6750338e9c9e88ae3cebea1))
* **eslint-config-appium-ts:** support ESLint 10 ([#22236](https://github.com/appium/appium/issues/22236)) ([4849ad0](https://github.com/appium/appium/commit/4849ad015d331a91dc7c013dda151ee432870293))
## [3.1.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@3.0.2...@appium/eslint-config-appium-ts@3.1.0) (2026-04-23)
@@ -102,6 +102,17 @@ export default defineConfig([
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Warn when type-only imports are not explicitly marked with `type`.
* @example `import type {Foo} from './foo'`
*/
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
},
],
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
@@ -1,7 +1,7 @@
{
"name": "@appium/eslint-config-appium-ts",
"type": "module",
"version": "3.1.0",
"version": "3.2.0",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
"eslint",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@eslint/compat": "2.0.5",
"@eslint/js": "9.39.4",
"@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "4.4.4",
@@ -39,12 +39,12 @@
"eslint-plugin-mocha": "11.2.0",
"eslint-plugin-n": "17.24.0",
"eslint-plugin-perfectionist": "5.9.0",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-promise": "7.3.0",
"eslint-plugin-unicorn": "64.0.0",
"typescript-eslint": "8.58.2"
"typescript-eslint": "8.59.1"
},
"peerDependencies": {
"eslint": "^9.0.0"
"eslint": "^9.0.0 || ^10.0.0"
},
"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.
## [6.0.3](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@6.0.2...@appium/execute-driver-plugin@6.0.3) (2026-05-06)
**Note:** Version bump only for package @appium/execute-driver-plugin
## [6.0.2](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@6.0.1...@appium/execute-driver-plugin@6.0.2) (2026-04-23)
+1 -1
View File
@@ -140,7 +140,7 @@ export class ExecuteDriverPlugin extends BasePlugin {
// and set up a race between the response from the child and the timeout
return await Promise.race([waitForResult(), waitForTimeout()]);
} catch (err: any) {
throw new Error(`Could not execute driver script. Original error was: ${err}`);
throw new Error(`Could not execute driver script. Original error was: ${err}`, {cause: err});
} finally {
// ensure we always cancel the timeout so that the timeout promise stops
// spinning and allows this process to die gracefully
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/execute-driver-plugin",
"version": "6.0.2",
"version": "6.0.3",
"description": "Plugin for batching and executing Appium driver commands",
"keywords": [
"automation",
@@ -40,7 +40,7 @@
},
"dependencies": {
"lodash": "4.18.1",
"webdriverio": "9.27.0"
"webdriverio": "9.27.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.
## [6.1.3](https://github.com/appium/appium/compare/@appium/fake-driver@6.1.2...@appium/fake-driver@6.1.3) (2026-05-06)
**Note:** Version bump only for package @appium/fake-driver
## [6.1.2](https://github.com/appium/appium/compare/@appium/fake-driver@6.1.1...@appium/fake-driver@6.1.2) (2026-04-23)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-driver",
"version": "6.1.2",
"version": "6.1.3",
"description": "Mock driver used internally by Appium for testing. Ignore",
"keywords": [
"automation",
@@ -41,7 +41,7 @@
},
"dependencies": {
"@xmldom/xmldom": "0.9.10",
"asyncbox": "6.1.0",
"asyncbox": "6.2.0",
"lodash": "4.18.1",
"xpath": "0.0.34"
},
+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.2.2](https://github.com/appium/appium/compare/@appium/fake-plugin@4.2.1...@appium/fake-plugin@4.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/fake-plugin
## [4.2.1](https://github.com/appium/appium/compare/@appium/fake-plugin@4.2.0...@appium/fake-plugin@4.2.1) (2026-04-23)
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-plugin",
"version": "4.2.1",
"version": "4.2.2",
"description": "A fake Appium plugin",
"keywords": [
"automation",
@@ -39,13 +39,13 @@
"test:unit": "mocha \"./test/unit/**/*.spec.*ts\""
},
"dependencies": {
"@appium/base-plugin": "3.2.1",
"@appium/support": "7.1.1",
"asyncbox": "6.1.0",
"@appium/base-plugin": "3.2.2",
"@appium/support": "7.2.0",
"asyncbox": "6.2.0",
"lodash": "4.18.1"
},
"devDependencies": {
"@appium/types": "1.3.1",
"@appium/types": "1.4.0",
"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.
## [4.2.2](https://github.com/appium/appium/compare/@appium/images-plugin@4.2.1...@appium/images-plugin@4.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/images-plugin
## [4.2.1](https://github.com/appium/appium/compare/@appium/images-plugin@4.2.0...@appium/images-plugin@4.2.1) (2026-04-23)
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/images-plugin",
"version": "4.2.1",
"version": "4.2.2",
"description": "Plugin for working with images and image elements in Appium",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:e2e": "mocha --exit --timeout 40s --slow 20s \"./test/e2e/**/*.spec.*ts\""
},
"dependencies": {
"@appium/opencv": "4.1.3",
"@appium/support": "7.1.1",
"@appium/opencv": "4.1.4",
"@appium/support": "7.2.0",
"lodash": "4.18.1",
"lru-cache": "11.3.5",
"sharp": "0.34.5"
+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.1.4](https://github.com/appium/appium/compare/@appium/opencv@4.1.3...@appium/opencv@4.1.4) (2026-05-06)
**Note:** Version bump only for package @appium/opencv
## [4.1.3](https://github.com/appium/appium/compare/@appium/opencv@4.1.2...@appium/opencv@4.1.3) (2026-04-23)
**Note:** Version bump only for package @appium/opencv
+2 -1
View File
@@ -429,7 +429,8 @@ export async function getImageOccurrence(
// Below error message, `Cannot find any occurrences` is referenced in find by image
throw new Error(
`Cannot find any occurrences of the partial image in the full image. ` +
`Original error: ${e?.message || String(e)}`
`Original error: ${e?.message || String(e)}`,
{cause: e}
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/opencv",
"version": "4.1.3",
"version": "4.1.4",
"description": "OpenCV-related helper library",
"keywords": [
"automation",
@@ -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.2.2](https://github.com/appium/appium/compare/@appium/plugin-test-support@1.2.1...@appium/plugin-test-support@1.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/plugin-test-support
## [1.2.1](https://github.com/appium/appium/compare/@appium/plugin-test-support@1.2.0...@appium/plugin-test-support@1.2.1) (2026-04-23)
**Note:** Version bump only for package @appium/plugin-test-support
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/plugin-test-support",
"version": "1.2.1",
"version": "1.2.2",
"description": "Testing utilities for Appium plugins",
"keywords": [
"automation",
@@ -39,12 +39,12 @@
"test:unit": "exit 0"
},
"devDependencies": {
"@appium/types": "1.3.1"
"@appium/types": "1.4.0"
},
"dependencies": {
"get-port": "7.2.0",
"log-symbols": "7.0.1",
"teen_process": "4.1.1"
"teen_process": "4.1.3"
},
"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.
## [2.2.2](https://github.com/appium/appium/compare/@appium/relaxed-caps-plugin@2.2.1...@appium/relaxed-caps-plugin@2.2.2) (2026-05-06)
**Note:** Version bump only for package @appium/relaxed-caps-plugin
## [2.2.1](https://github.com/appium/appium/compare/@appium/relaxed-caps-plugin@2.2.0...@appium/relaxed-caps-plugin@2.2.1) (2026-04-23)
**Note:** Version bump only for package @appium/relaxed-caps-plugin
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/relaxed-caps-plugin",
"version": "2.2.1",
"version": "2.2.2",
"description": "An Appium plugin that loosens requirements for vendor prefixes on caps",
"keywords": [
"automation",
@@ -42,7 +42,7 @@
"lodash": "4.18.1"
},
"devDependencies": {
"@appium/types": "1.3.1"
"@appium/types": "1.4.0"
},
"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.
## [1.1.1](https://github.com/appium/appium/compare/@appium/schema@1.1.0...@appium/schema@1.1.1) (2026-05-06)
**Note:** Version bump only for package @appium/schema
## [1.1.0](https://github.com/appium/appium/compare/@appium/schema@1.0.1...@appium/schema@1.1.0) (2026-03-08)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/schema",
"version": "1.1.0",
"version": "1.1.1",
"description": "Appium Configuration Schema",
"keywords": [
"automation",
@@ -49,9 +49,10 @@ async function write() {
// Use createRequire to require CommonJS module from ESM
const require = createRequire(import.meta.url);
({AppiumConfigJsonSchema: schema} = require(SCHEMA_SRC));
} catch {
} catch (e) {
throw new Error(
`${error} Failed to read ${SCHEMA_SRC}; did you execute \`npm run build\` first?`
`${error} Failed to read ${SCHEMA_SRC}; did you execute \`npm run build\` first?`,
{cause: e}
);
}
@@ -62,7 +63,10 @@ async function write() {
await writeFile(OUTPUT_PATH, json);
console.log(`${info} Wrote JSON schema to ${OUTPUT_PATH}`);
} catch (err) {
throw new Error(`${error} Failed to write JSON schema to ${OUTPUT_PATH}: ${err.message}`);
throw new Error(
`${error} Failed to write JSON schema to ${OUTPUT_PATH}: ${err.message}`,
{cause: err}
);
}
}
+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/storage-plugin@1.1.1...@appium/storage-plugin@1.1.2) (2026-05-06)
**Note:** Version bump only for package @appium/storage-plugin
## [1.1.1](https://github.com/appium/appium/compare/@appium/storage-plugin@1.1.0...@appium/storage-plugin@1.1.1) (2026-04-23)
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/storage-plugin",
"version": "1.1.1",
"version": "1.1.2",
"description": "Server-side storage plugin for Appium",
"keywords": [
"automation",
@@ -40,9 +40,9 @@
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.ts\""
},
"dependencies": {
"@appium/support": "7.1.1",
"@appium/support": "7.2.0",
"async-lock": "1.4.1",
"asyncbox": "6.1.0",
"asyncbox": "6.2.0",
"lodash": "4.18.1",
"lru-cache": "11.3.5",
"ws": "8.20.0"
+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.0](https://github.com/appium/appium/compare/@appium/support@7.1.1...@appium/support@7.2.0) (2026-05-06)
### Features
* **support:** Add various helper utils ([#22213](https://github.com/appium/appium/issues/22213)) ([2c0fb7a](https://github.com/appium/appium/commit/2c0fb7a2e4a8652e51ce010e8832153c49801a9d))
* **support:** Replace lodash with native alternatives ([#22225](https://github.com/appium/appium/issues/22225)) ([63218cd](https://github.com/appium/appium/commit/63218cdac639c3a15584e5f815f15a8c9f2db049))
## [7.1.1](https://github.com/appium/appium/compare/@appium/support@7.1.0...@appium/support@7.1.1) (2026-04-23)
+1 -2
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import {createSupportsColor} from 'supports-color';
import {Console as NodeConsole} from 'node:console';
import type {Color} from '@colors/colors';
@@ -77,7 +76,7 @@ export class CliConsole {
* Returns `undefined` if `msg` is `undefined`.
*/
decorate(msg: string | undefined, symbol?: SymbolKey): string | undefined {
if (!_.isString(msg) || !_.isString(symbol) || !this.#useSymbols) {
if (typeof msg !== 'string' || typeof symbol !== 'string' || !this.#useSymbols) {
return msg;
}
+5 -5
View File
@@ -1,8 +1,8 @@
import _ from 'lodash';
import {homedir} from 'node:os';
import path from 'node:path';
import {readPackage, type NormalizedPackageJson} from 'read-pkg';
import * as semver from 'semver';
import {memoize} from './util';
/**
* Path to the default `APPIUM_HOME` dir (`~/.appium`).
@@ -36,7 +36,7 @@ export async function hasAppiumDependency(cwd: string): Promise<boolean> {
*
* Looks at `dependencies` and `devDependencies` for `appium`.
*/
export const findAppiumDependencyPackage = _.memoize(
export const findAppiumDependencyPackage = memoize(
async function findAppiumDependencyPackage(
cwd: string = process.cwd(),
acceptableVersionRange: string | semver.Range = '>=2.0.0-beta'
@@ -76,7 +76,7 @@ export const findAppiumDependencyPackage = _.memoize(
/**
* Read a `package.json` in dir `cwd`. If none found, return `undefined`.
*/
export const readPackageInDir = _.memoize(
export const readPackageInDir = memoize(
async function _readPackageInDir(cwd: string): Promise<NormalizedPackageJson> {
return await readPackage({cwd, normalize: true});
}
@@ -90,7 +90,7 @@ export const readPackageInDir = _.memoize(
*
* All returned paths will be absolute.
*/
export const resolveAppiumHome = _.memoize(
export const resolveAppiumHome = memoize(
async function _resolveAppiumHome(cwd: string = process.cwd()): Promise<string> {
if (!path.isAbsolute(cwd)) {
throw new TypeError('`cwd` parameter must be an absolute path');
@@ -110,7 +110,7 @@ export const resolveAppiumHome = _.memoize(
* The assumption is that, if `appiumHome` has been provided, it was resolved via {@link resolveAppiumHome `resolveAppiumHome()`}! If unsure,
* don't pass a parameter and let `resolveAppiumHome()` handle it.
*/
export const resolveManifestPath = _.memoize(
export const resolveManifestPath = memoize(
async function _resolveManifestPath(
appiumHome?: string
): Promise<string> {
+6 -4
View File
@@ -20,7 +20,6 @@ import {glob} from 'glob';
import type {GlobOptions} from 'glob';
import klaw from 'klaw';
import type {Walker} from 'klaw';
import _ from 'lodash';
import ncp from 'ncp';
import {packageDirectorySync} from 'package-directory';
import path from 'node:path';
@@ -30,14 +29,14 @@ import which from 'which';
import log from './logger';
import {Timer} from './timing';
import {isWindows} from './system';
import {pluralize} from './util';
import {memoize, pluralize} from './util';
const ncpAsync = promisify(ncp) as (
source: string,
dest: string,
opts?: ncp.Options
) => Promise<void>;
const findRootCached = _.memoize(
const findRootCached = memoize(
packageDirectorySync,
(opts: {cwd?: string} | undefined) => opts?.cwd
);
@@ -213,7 +212,10 @@ export const fs = {
fromStat = await fsPromises.stat(from);
} catch (err) {
if (isErrnoException(err) && err.code === 'ENOENT') {
throw new Error(`The source path '${from}' does not exist or is not accessible`);
throw new Error(
`The source path '${from}' does not exist or is not accessible`,
{cause: err}
);
}
throw err;
}
+2 -1
View File
@@ -14,7 +14,8 @@ export function requireSharp(): typeof sharp {
throw new Error(
`Cannot load the 'sharp' module needed for images processing. ` +
`Consider visiting https://sharp.pixelplumbing.com/install ` +
`for troubleshooting. Original error: ${message}`
`for troubleshooting. Original error: ${message}`,
{cause: err}
);
}
}
+7 -1
View File
@@ -73,7 +73,13 @@ export type {
} from './net';
export type {InstallPackageOpts, ExecOpts, NpmInstallReceipt} from './npm';
export type {Affixes, OpenedAffixes} from './tempdir';
export type {PluralizeOptions, EncodingOptions, LockFileOptions, NonEmptyString} from './util';
export type {
PluralizeOptions,
EncodingOptions,
LockFileOptions,
TruncateStringOptions,
NonEmptyString,
} from './util';
export type {
ExtractAllOptions,
ZipEntry,
+5 -5
View File
@@ -8,7 +8,6 @@ import type {
AppiumLoggerLevel,
AppiumLoggerPrefix,
} from '@appium/types';
import _ from 'lodash';
export const LEVELS: readonly AppiumLoggerLevel[] = [
'silly',
@@ -27,6 +26,7 @@ interface GlobalWithNpmlog {
}
const globalWithNpmlog = globalThis as typeof globalThis & GlobalWithNpmlog;
const noop = () => {};
// mock log object is used in testing mode to silence the output
const MOCK_LOG = {
@@ -35,9 +35,9 @@ const MOCK_LOG = {
Promise.resolve({issues: [], rules: []}),
level: 'verbose',
prefix: '',
log: _.noop,
log: noop,
}),
...(_.fromPairs(LEVELS.map((l) => [l, _.noop])) as Record<
...(Object.fromEntries(LEVELS.map((l) => [l, noop])) as Record<
AppiumLoggerLevel,
(...args: any[]) => void
>),
@@ -58,7 +58,7 @@ export function getLogger(prefix: AppiumLoggerPrefix | null = null): AppiumLogge
prefix: prefix ?? undefined,
errorWithException(...args: any[]) {
this.error(...args);
return _.isError(args[0]) ? args[0] : new Error(args.join('\n'));
return args[0] instanceof Error ? args[0] : new Error(args.join('\n'));
},
errorAndThrow(...args: any[]) {
throw this.errorWithException(...args);
@@ -144,7 +144,7 @@ function getFinalPrefix(
prefix: AppiumLoggerPrefix | null | undefined,
shouldLogTimestamp = false
): string {
const result = (_.isFunction(prefix) ? prefix() : prefix) ?? '';
const result = (typeof prefix === 'function' ? prefix() : prefix) ?? '';
if (!shouldLogTimestamp) {
return result;
}
+9 -7
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import log from './logger';
import B from 'bluebird';
import {requireSharp} from './image-util';
@@ -18,10 +17,11 @@ async function initMJpegConsumer(): Promise<MJpegConsumerConstructor> {
if (!MJpegConsumer) {
try {
MJpegConsumer = (await requirePackage('mjpeg-consumer')) as MJpegConsumerConstructor;
} catch {
} catch (e) {
throw new Error(
'mjpeg-consumer module is required to use MJPEG-over-HTTP features. ' +
'Please install it first (npm i -g mjpeg-consumer) and restart Appium.'
'Please install it first (npm i -g mjpeg-consumer) and restart Appium.',
{cause: e}
);
}
}
@@ -29,6 +29,7 @@ async function initMJpegConsumer(): Promise<MJpegConsumerConstructor> {
}
const MJPEG_SERVER_TIMEOUT_MS = 10000;
const noop = () => {};
/** Class which stores the last bit of data streamed into it */
export class MJpegStream extends Writable {
@@ -50,7 +51,7 @@ export class MJpegStream extends Writable {
*/
constructor(
mJpegUrl: string,
errorHandler: (err: Error) => void = _.noop,
errorHandler: (err: Error) => void = noop,
options: WritableOptions = {}
) {
super(options);
@@ -61,7 +62,7 @@ export class MJpegStream extends Writable {
get lastChunkBase64(): string | null {
const lastChunk = this.lastChunk;
if (lastChunk && !_.isEmpty(lastChunk) && _.isBuffer(lastChunk)) {
if (lastChunk && lastChunk.length > 0 && Buffer.isBuffer(lastChunk)) {
return lastChunk.toString('base64');
}
return null;
@@ -69,7 +70,7 @@ export class MJpegStream extends Writable {
async lastChunkPNG(): Promise<Buffer | null> {
const chunk = this.lastChunk;
if (!chunk || _.isEmpty(chunk) || !_.isBuffer(chunk)) {
if (!chunk || chunk.length === 0 || !Buffer.isBuffer(chunk)) {
return null;
}
try {
@@ -118,7 +119,8 @@ export class MJpegStream extends Writable {
message = String(e);
}
throw new Error(
`Cannot connect to the MJPEG stream at ${url}. Original error: ${message}`
`Cannot connect to the MJPEG stream at ${url}. Original error: ${message}`,
{cause: e}
);
}
+21 -15
View File
@@ -1,6 +1,5 @@
import _ from 'lodash';
import {fs} from './fs';
import {toReadableSizeString} from './util';
import {isPlainObject, toReadableSizeString} from './util';
import log from './logger';
import Ftp from 'jsftp';
import {Timer} from './timing';
@@ -87,7 +86,7 @@ export async function uploadFile(
if (isHttpUploadOptions(uploadOptions, url)) {
if (!uploadOptions.fileFieldName) {
uploadOptions.headers = {
...(_.isPlainObject(uploadOptions.headers) ? uploadOptions.headers : {}),
...(isPlainObject(uploadOptions.headers) ? uploadOptions.headers : {}),
'Content-Length': size,
};
}
@@ -132,7 +131,7 @@ export async function downloadFile(
if (axiosAuth) {
requestOpts.auth = axiosAuth;
}
if (_.isPlainObject(headers)) {
if (isPlainObject(headers)) {
requestOpts.headers = headers as RawAxiosRequestConfig['headers'];
}
@@ -154,7 +153,7 @@ export async function downloadFile(
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Cannot download the file from ${remoteUrl}: ${message}`);
throw new Error(`Cannot download the file from ${remoteUrl}: ${message}`, {cause: err});
}
const {size} = await fs.stat(dstPath);
@@ -179,13 +178,16 @@ export async function downloadFile(
}
function toAxiosAuth(auth: AuthLike | undefined): AxiosBasicCredentials | null {
if (!auth || !_.isPlainObject(auth)) {
if (!auth || !isPlainObject(auth)) {
return null;
}
const username = 'username' in auth ? auth.username : auth.user;
const password = 'password' in auth ? auth.password : auth.pass;
return username && password ? {username, password} : null;
if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) {
return null;
}
return {username, password};
}
async function uploadFileToHttp(
@@ -218,13 +220,13 @@ async function uploadFileToHttp(
const form = new FormData();
if (formFields) {
let pairs: [string, unknown][] = [];
if (_.isArray(formFields)) {
if (Array.isArray(formFields)) {
pairs = formFields as [string, unknown][];
} else if (_.isPlainObject(formFields)) {
pairs = _.toPairs(formFields);
} else if (isPlainObject(formFields)) {
pairs = Object.entries(formFields);
}
for (const [key, value] of pairs) {
if (_.toLower(key) !== _.toLower(fileFieldName)) {
if (key.toLowerCase() !== fileFieldName?.toLowerCase()) {
form.append(key, value as string | Buffer);
}
}
@@ -232,19 +234,23 @@ async function uploadFileToHttp(
// AWS S3 POST upload requires this to be the last field; do not move before formFields.
form.append(fileFieldName, localFileStream);
requestOpts.headers = {
...(_.isPlainObject(headers) ? headers : {}),
...(isPlainObject(headers) ? headers : {}),
...form.getHeaders(),
};
requestOpts.data = form;
} else {
if (_.isPlainObject(headers)) {
if (isPlainObject(headers)) {
requestOpts.headers = headers;
}
requestOpts.data = localFileStream;
}
log.debug(
`Performing ${method} to ${href} with options (excluding data): ` +
JSON.stringify(_.omit(requestOpts, ['data']))
JSON.stringify((() => {
const requestOptsWithoutData = {...requestOpts} as Record<string, unknown>;
delete requestOptsWithoutData.data;
return requestOptsWithoutData;
})())
);
const {status, statusText} = await axios(requestOpts);
@@ -261,7 +267,7 @@ async function uploadFileToFtp(
const ftpOpts: {host: string; port: number; user?: string; pass?: string} = {
host: hostname ?? '',
port: port !== undefined && port !== '' ? _.parseInt(port, 10) : 21,
port: port !== undefined && port !== '' ? Number.parseInt(port, 10) : 21,
};
if (auth?.user && auth?.pass) {
ftpOpts.user = auth.user;
+7 -8
View File
@@ -1,6 +1,5 @@
import {isWindows} from './system';
import log from './logger';
import _ from 'lodash';
import {exec} from 'teen_process';
import path from 'node:path';
import _fs from 'node:fs';
@@ -156,7 +155,7 @@ async function linkGlobalPackage(packageName: string): Promise<void> {
if (e.stderr) {
log.debug(e.stderr);
}
throw new Error(msg);
throw new Error(msg, {cause: err});
}
}
@@ -165,14 +164,14 @@ function extractAllProperties(obj: object): (string | symbol)[] {
for (const prop in obj) {
stringProperties.push(prop);
}
if (_.isFunction(Object.getOwnPropertySymbols)) {
if (typeof Object.getOwnPropertySymbols === 'function') {
stringProperties.push(...Object.getOwnPropertySymbols(obj));
}
return stringProperties;
}
function _getSizeOfObject(seen: WeakSet<object>, object: object): number {
if (_.isNil(object)) {
if (object == null) {
return 0;
}
@@ -181,7 +180,7 @@ function _getSizeOfObject(seen: WeakSet<object>, object: object): number {
const calc = getCalculator(seen);
for (const key of properties) {
const val = (object as Record<string | symbol, unknown>)[key];
if (typeof val === 'object' && !_.isNil(val)) {
if (typeof val === 'object' && val != null) {
if (seen.has(val as object)) {
continue;
}
@@ -203,7 +202,7 @@ function _getSizeOfObject(seen: WeakSet<object>, object: object): number {
function getCalculator(seen: WeakSet<object>): SizeCalculator {
return function calculator(obj: unknown): number {
if (_.isBuffer(obj)) {
if (Buffer.isBuffer(obj)) {
return (obj as Buffer).length;
}
@@ -215,11 +214,11 @@ function getCalculator(seen: WeakSet<object>): SizeCalculator {
case 'number':
return ECMA_SIZES.NUMBER;
case 'symbol':
return _.isFunction(Symbol.keyFor) && Symbol.keyFor(obj)
return typeof Symbol.keyFor === 'function' && Symbol.keyFor(obj)
? (Symbol.keyFor(obj) as string).length * ECMA_SIZES.STRING
: (obj.toString().length - 8) * ECMA_SIZES.STRING;
case 'object':
return _.isArray(obj)
return Array.isArray(obj)
? obj.map(getCalculator(seen)).reduce((acc, curr) => acc + curr, 0)
: _getSizeOfObject(seen, obj as object);
default:
+5 -3
View File
@@ -104,7 +104,8 @@ export class NPM {
} catch (e) {
const {stdout = '', stderr = '', code = null} = e as ExecError;
throw new Error(
`npm command '${argsCopy.join(' ')}' failed with code ${code}.\n\nSTDOUT:\n${stdout.trim()}\n\nSTDERR:\n${stderr.trim()}`
`npm command '${argsCopy.join(' ')}' failed with code ${code}.\n\nSTDOUT:\n${stdout.trim()}\n\nSTDERR:\n${stderr.trim()}`,
{cause: e}
);
}
return ret;
@@ -245,11 +246,12 @@ export class NPM {
const pkgJson = await fs.readFile(pkgJsonPath, 'utf8');
const pkg = JSON.parse(pkgJson) as PackageJson;
return {installPath: path.dirname(pkgJsonPath), pkg};
} catch {
} catch (e) {
throw new Error(
'The package was not downloaded correctly; its package.json ' +
'did not exist or was unreadable. We looked for it at ' +
pkgJsonPath
pkgJsonPath,
{cause: e}
);
}
}
+11 -11
View File
@@ -1,9 +1,9 @@
import xmlplist from 'plist';
import {build as plistBuild, parse as plistParse} from 'plist';
import bplistCreate from 'bplist-creator';
import {parseFile, parseBuffer} from 'bplist-parser';
import {fs} from './fs';
import log from './logger';
import _ from 'lodash';
import {truncateString} from './util';
const BPLIST_IDENTIFIER = {
BUFFER: Buffer.from('bplist00'),
@@ -37,7 +37,7 @@ export async function parsePlistFile(
return {};
}
let obj: object = {};
let obj: object;
let type = 'binary';
try {
const parsed = await parseFile(plist);
@@ -85,8 +85,8 @@ export async function updatePlistFile(
} catch (err) {
throw log.errorWithException(`Could not update plist: ${(err as Error).message}`);
}
_.extend(obj, updatedFields);
const newPlist = binary ? bplistCreate(obj) : xmlplist.build(obj);
Object.assign(obj as Record<string, unknown>, updatedFields);
const newPlist = binary ? bplistCreate(obj) : plistBuild(obj);
try {
await fs.writeFile(plist, newPlist);
} catch (err) {
@@ -128,7 +128,7 @@ export function createPlist(object: object, binary = false): Buffer | string {
if (binary) {
return createBinaryPlist(object);
}
return xmlplist.build(object);
return plistBuild(object);
}
/**
@@ -141,7 +141,7 @@ export function createPlist(object: object, binary = false): Buffer | string {
export function parsePlist(data: string | Buffer): object {
const textPlist = getXmlPlist(data);
if (textPlist) {
return xmlplist.parse(textPlist);
return plistParse(textPlist);
}
const binaryPlist = getBinaryPlist(data);
@@ -150,17 +150,17 @@ export function parsePlist(data: string | Buffer): object {
}
throw new Error(
`Unknown type of plist, data: ${_.truncate(data.toString(), {length: 200})}`
`Unknown type of plist, data: ${truncateString(data.toString(), {length: 200})}`
);
}
async function parseXmlPlistFile(plistFilename: string): Promise<object> {
const xmlContent = await fs.readFile(plistFilename, 'utf8');
return xmlplist.parse(xmlContent);
return plistParse(xmlContent);
}
function getXmlPlist(data: string | Buffer): string | null {
if (_.isString(data) && data.startsWith(PLIST_IDENTIFIER.TEXT)) {
if (typeof data === 'string' && data.startsWith(PLIST_IDENTIFIER.TEXT)) {
return data;
}
if (
@@ -173,7 +173,7 @@ function getXmlPlist(data: string | Buffer): string | null {
}
function getBinaryPlist(data: string | Buffer): Buffer | null {
if (_.isString(data) && data.startsWith(BPLIST_IDENTIFIER.TEXT)) {
if (typeof data === 'string' && data.startsWith(BPLIST_IDENTIFIER.TEXT)) {
return Buffer.from(data);
}
+8 -2
View File
@@ -28,7 +28,10 @@ export async function getProcessIds(appName: string): Promise<number[]> {
} catch (err) {
const code = (err as ExecError).code;
if (code !== 1) {
throw new Error(`Error getting process ids for app '${appName}': ${(err as Error).message}`);
throw new Error(
`Error getting process ids for app '${appName}': ${(err as Error).message}`,
{cause: err}
);
}
pids = [];
}
@@ -56,7 +59,10 @@ export async function killProcess(appName: string, force = false): Promise<void>
} catch (err) {
const code = (err as ExecError).code;
if (code !== 1) {
throw new Error(`Error killing app '${appName}' with pkill: ${(err as Error).message}`);
throw new Error(
`Error killing app '${appName}' with pkill: ${(err as Error).message}`,
{cause: err}
);
}
}
}
+2 -3
View File
@@ -1,5 +1,4 @@
import {exec} from 'teen_process';
import _ from 'lodash';
import os from 'node:os';
const VERSION_PATTERN = /^(\d+\.\d+)/m;
@@ -29,7 +28,7 @@ export function isLinux(): boolean {
* Whether the current Windows process is 64-bit (or WOW64).
*/
export function isOSWin64(): boolean {
return process.arch === 'x64' || _.has(process.env, 'PROCESSOR_ARCHITEW6432');
return process.arch === 'x64' || Object.hasOwn(process.env, 'PROCESSOR_ARCHITEW6432');
}
/**
@@ -43,7 +42,7 @@ export async function macOsxVersion(): Promise<string> {
try {
stdout = (await exec('sw_vers', ['-productVersion'])).stdout.trim();
} catch (err) {
throw new Error(`Could not detect Mac OS X Version: ${err}`);
throw new Error(`Could not detect Mac OS X Version: ${err}`, {cause: err});
}
const versionMatch = VERSION_PATTERN.exec(stdout);
+2 -2
View File
@@ -2,9 +2,9 @@
import {fs} from './fs';
import os from 'node:os';
import nodePath from 'node:path';
import _ from 'lodash';
import {constants} from 'node:fs';
import log from './logger';
import {memoize} from './util';
const RDWR_EXCL = constants.O_CREAT | constants.O_TRUNC | constants.O_RDWR | constants.O_EXCL;
@@ -61,7 +61,7 @@ export const openDir = tempDir;
*
* @returns The same temp directory path on every call.
*/
export const staticDir = _.memoize(async function staticDir (): Promise<string> {
export const staticDir = memoize(async function staticDir (): Promise<string> {
return tempDir();
});
+3 -5
View File
@@ -1,5 +1,3 @@
import _ from 'lodash';
const NS_PER_S = 1e9;
const NS_PER_MS = 1e6;
@@ -65,7 +63,7 @@ export class Timer {
* @return {Timer} The current instance, for chaining
*/
start(): this {
if (!_.isNull(this._startTime)) {
if (this._startTime !== null) {
throw new Error('Timer has already been started.');
}
this._startTime = process.hrtime.bigint();
@@ -78,12 +76,12 @@ export class Timer {
* @return {Duration} the duration
*/
getDuration(): Duration {
if (_.isNull(this._startTime)) {
if (this._startTime === null) {
throw new Error('Unable to get duration. Timer was not started');
}
let nanoDuration: number;
if (_.isArray(this._startTime)) {
if (Array.isArray(this._startTime)) {
// startTime was created using process.hrtime()
const [seconds, nanos] = process.hrtime(this._startTime as [number, number]);
nanoDuration = seconds * NS_PER_S + nanos;
+171 -18
View File
@@ -1,9 +1,9 @@
import B from 'bluebird';
import _ from 'lodash';
import {randomUUID} from 'node:crypto';
import os from 'node:os';
import path from 'node:path';
import stream from 'node:stream';
import {promisify} from 'node:util';
import {isDeepStrictEqual, promisify} from 'node:util';
import {asyncmap} from 'asyncbox';
import {fs} from './fs';
import * as semver from 'semver';
@@ -11,7 +11,7 @@ import {quote as shellQuote, parse as shellParse} from 'shell-quote';
export {shellParse};
import pluralizeLib from 'pluralize';
import {Base64Encode} from 'base64-stream';
export {v1 as uuidV1, v3 as uuidV3, v4 as uuidV4, v5 as uuidV5} from 'uuid';
import {v1 as uuidV1Lib, v3 as uuidV3Lib, v5 as uuidV5Lib} from 'uuid';
import * as _lockfile from 'lockfile';
import type {Element} from '@appium/types';
@@ -28,6 +28,36 @@ export const GiB = MiB * 1024;
/** A string which is never `''`. */
export type NonEmptyString<T extends string = string> = T extends '' ? never : T;
/**
* @deprecated This helper is slated for removal. Please migrate callers away from UUID v1.
*/
export function uuidV1(...args: Parameters<typeof uuidV1Lib>): ReturnType<typeof uuidV1Lib> {
return uuidV1Lib(...args);
}
/**
* @deprecated This helper is slated for removal. Please migrate callers away from UUID v3.
*/
export function uuidV3(...args: Parameters<typeof uuidV3Lib>): ReturnType<typeof uuidV3Lib> {
return uuidV3Lib(...args);
}
/**
* Generates a v4 UUID using Node.js crypto.
*
* @returns A UUID v4 string
*/
export function uuidV4(): string {
return randomUUID();
}
/**
* @deprecated This helper is slated for removal. Please migrate callers away from UUID v5.
*/
export function uuidV5(...args: Parameters<typeof uuidV5Lib>): ReturnType<typeof uuidV5Lib> {
return uuidV5Lib(...args);
}
/**
* Type guard: returns true if the value is a non-empty string.
*
@@ -35,7 +65,7 @@ export type NonEmptyString<T extends string = string> = T extends '' ? never : T
* @returns `true` if `val` is a string with at least one character
*/
export function hasContent(val: unknown): val is NonEmptyString {
return _.isString(val) && val !== '';
return typeof val === 'string' && val !== '';
}
/**
@@ -45,10 +75,124 @@ export function hasContent(val: unknown): val is NonEmptyString {
* @returns `true` if `val` is non-null and non-undefined (and not NaN for numbers)
*/
export function hasValue<T>(val: T): val is NonNullable<T> {
if (_.isNumber(val)) {
return !_.isNaN(val);
if (typeof val === 'number') {
return !Number.isNaN(val);
}
return !_.isUndefined(val) && !_.isNull(val);
return val !== undefined && val !== null;
}
/**
* Creates a memoized version of a function.
*
* @param fn - Function to memoize
* @param resolver - Optional cache key resolver. If omitted, the first argument is used as the cache key.
* @returns Memoized function with a mutable `.cache` map (compatible with lodash-style cache resets in tests).
*/
export function memoize<Fn extends (...args: any[]) => any>(
fn: Fn,
resolver?: (...args: Parameters<Fn>) => unknown
): Fn & {cache: Map<unknown, ReturnType<Fn>>} {
const memoizedFn = (function (this: unknown, ...args: Parameters<Fn>) {
const key = resolver ? resolver.apply(this, args) : args[0];
if (memoizedFn.cache.has(key)) {
return memoizedFn.cache.get(key) as ReturnType<Fn>;
}
const result = fn.apply(this, args);
memoizedFn.cache.set(key, result);
return result;
}) as unknown as Fn & {cache: Map<unknown, ReturnType<Fn>>};
memoizedFn.cache = new Map<unknown, ReturnType<Fn>>();
return memoizedFn;
}
/**
* Returns true if the value is a plain object (Object prototype or null prototype).
*
* @param value - Value to check
* @returns `true` if the value is a plain object
*/
export function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
/**
* Returns true when the value has no elements/properties.
*
* @param value - Value to check
* @returns `true` if the value is empty
*/
export function isEmpty(value: unknown): boolean {
if (value == null) {
return true;
}
if (typeof value === 'string' || Array.isArray(value) || Buffer.isBuffer(value)) {
return value.length === 0;
}
if (value instanceof Map || value instanceof Set) {
return value.size === 0;
}
if (typeof value === 'object' || typeof value === 'function') {
return Object.keys(value).length === 0;
}
return true;
}
/**
* Performs a deep equality check between two values.
*
* @param left - First value
* @param right - Second value
* @returns `true` when values are deeply equal
*/
export function isEqual(left: unknown, right: unknown): boolean {
return isDeepStrictEqual(left, right);
}
/**
* Escapes RegExp special characters in a string.
*
* @param value - Input string
* @returns Escaped string safe for RegExp source
*/
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Returns a duplicate-free copy of the input array.
*
* @param values - Input array
* @returns New array with unique values preserving input order
*/
export function uniq<T>(values: readonly T[]): T[] {
return [...new Set(values)];
}
/**
* Truncates a string to a maximum length.
*
* @param value - Input string
* @param options - Truncation options or max length
* @returns Truncated string
*/
export function truncateString(value: string, options: TruncateStringOptions | number = {}): string {
const normalizedOptions = typeof options === 'number' ? {length: options} : options;
const {length, omission = '…'} = normalizedOptions;
const stringValue =
value == null ? '' : typeof value === 'number' && Object.is(value, -0) ? '-0' : String(value);
const maxLength = length ?? 30;
if (maxLength <= 0) {
return omission;
}
if (stringValue.length <= maxLength) {
return stringValue;
}
const contentLength = maxLength - omission.length;
return contentLength > 0 ? `${stringValue.slice(0, contentLength)}${omission}` : omission;
}
/**
@@ -177,7 +321,7 @@ export function jsonStringify(
replacer: ((key: string, value: unknown) => unknown) | null = null,
space: number | string = 2
): string {
const replacerFunc = _.isFunction(replacer) ? replacer : (_k: string, v: unknown) => v;
const replacerFunc = typeof replacer === 'function' ? replacer : (_k: string, v: unknown) => v;
const bufferToJSON = Buffer.prototype.toJSON;
delete (Buffer.prototype as Record<string, unknown>).toJSON;
@@ -204,7 +348,7 @@ export function jsonStringify(
export function unwrapElement(el: Element | string): string {
const elObj = el as unknown as Record<string, string>;
for (const propName of [W3C_WEB_ELEMENT_IDENTIFIER, 'ELEMENT']) {
if (_.has(elObj, propName)) {
if (Object.hasOwn(elObj, propName)) {
return elObj[propName];
}
}
@@ -238,11 +382,11 @@ export function filterObject<T extends Record<string, unknown>>(
obj: T,
predicate?: ((value: unknown, obj: T) => boolean) | unknown
): Partial<T> {
const newObj = _.clone(obj) as Record<string, unknown>;
const newObj = {...obj} as Record<string, unknown>;
let pred: (v: unknown, o: T) => boolean;
if (_.isUndefined(predicate)) {
pred = (v) => !_.isUndefined(v);
} else if (!_.isFunction(predicate)) {
if (predicate === undefined) {
pred = (v) => v !== undefined;
} else if (typeof predicate !== 'function') {
const valuePredicate = predicate;
pred = (v) => v === valuePredicate;
} else {
@@ -374,6 +518,14 @@ export interface LockFileOptions {
tryRecovery?: boolean;
}
/** Options for truncateString(). */
export interface TruncateStringOptions {
/** Maximum length of the resulting string. Default 30. */
length?: number;
/** Suffix appended to truncated strings. Default "…". */
omission?: string;
}
/** Guard function that runs the given behavior under the lock. */
type LockFileGuardFn<T> = (behavior: () => Promise<T> | T) => Promise<T>;
@@ -415,7 +567,7 @@ export function compareVersions(
* @returns Quoted string suitable for shell parsing
*/
export function quote(args: string | string[]): string {
return shellQuote(_.castArray(args));
return shellQuote(Array.isArray(args) ? args : [args]);
}
/**
@@ -432,9 +584,9 @@ export function pluralize(
options: PluralizeOptions | boolean = {}
): string {
let inclusive = false;
if (_.isBoolean(options)) {
if (typeof options === 'boolean') {
inclusive = options;
} else if (_.isBoolean(options?.inclusive)) {
} else if (typeof options?.inclusive === 'boolean') {
inclusive = options.inclusive;
}
return pluralizeLib(word, count, inclusive);
@@ -537,13 +689,14 @@ export function getLockFileGuard<T>(
acquired = true;
} catch (e) {
const err = e as Error;
if (_.includes(err.message, 'EEXIST') && tryRecovery && !triedRecovery) {
if (err.message?.includes('EEXIST') && tryRecovery && !triedRecovery) {
_lockfile.unlockSync(lockFile);
triedRecovery = true;
} else {
throw new Error(
`Could not acquire lock on '${lockFile}' after ${timeout}s. ` +
`Original error: ${err.message}`
`Original error: ${err.message}`,
{cause: e}
);
}
}
+4 -5
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import {promisify} from 'node:util';
import * as yauzl from 'yauzl';
import archiver from 'archiver';
@@ -9,7 +8,7 @@ import {pipeline} from 'node:stream/promises';
import {fs} from './fs';
import {isWindows} from './system';
import {Base64Encode} from 'base64-stream';
import {isSubPath, toReadableSizeString, GiB} from './util';
import {isSubPath, memoize, toReadableSizeString, GiB} from './util';
import {Timer} from './timing';
import log from './logger';
import getStream from 'get-stream';
@@ -554,8 +553,8 @@ async function extractWithSystemUnzip(zipFilePath: string, destDir: string): Pro
let executablePath: string;
try {
executablePath = await getExecutablePath(isWindowsHost ? 'powershell.exe' : 'unzip');
} catch {
throw new Error('Could not find system unzip');
} catch (e) {
throw new Error('Could not find system unzip', {cause: e});
}
if (isWindowsHost) {
@@ -581,7 +580,7 @@ async function extractWithSystemUnzip(zipFilePath: string, destDir: string): Pro
* Finds and memoizes the full path to the given executable.
* Rejects if it is not found.
*/
const getExecutablePath = _.memoize(
const getExecutablePath = memoize(
/**
* @returns Full Path to the executable
*/
+6 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/support",
"version": "7.1.1",
"version": "7.2.0",
"description": "Support libs used across Appium packages",
"keywords": [
"automation",
@@ -42,11 +42,11 @@
"dependencies": {
"@appium/logger": "2.0.7",
"@appium/tsconfig": "1.1.2",
"@appium/types": "1.3.1",
"@appium/types": "1.4.0",
"@colors/colors": "1.6.0",
"archiver": "7.0.1",
"asyncbox": "6.1.0",
"axios": "1.15.0",
"asyncbox": "6.2.0",
"axios": "1.16.0",
"base64-stream": "1.0.0",
"bluebird": "3.7.2",
"bplist-creator": "0.1.1",
@@ -57,11 +57,10 @@
"jsftp": "2.1.3",
"klaw": "4.1.0",
"lockfile": "1.0.4",
"lodash": "4.18.1",
"log-symbols": "7.0.1",
"ncp": "2.0.0",
"package-directory": "8.2.0",
"plist": "3.1.0",
"plist": "4.0.0",
"pluralize": "8.0.0",
"read-pkg": "10.1.0",
"resolve-from": "5.0.0",
@@ -69,7 +68,7 @@
"semver": "7.7.4",
"shell-quote": "1.8.3",
"supports-color": "10.2.2",
"teen_process": "4.1.1",
"teen_process": "4.1.3",
"type-fest": "5.6.0",
"uuid": "14.0.0",
"which": "6.0.1",
+3 -4
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import {sleep} from 'asyncbox';
import http from 'node:http';
import {expect, use} from 'chai';
@@ -29,7 +28,7 @@ function initMJpegServer(port: number, intMs = 300, times = 20): http.Server {
// Just send the same jpeg over and over.
for (let i = 0; i < times; i++) {
await sleep(intMs);
mJpegReqHandler._write(jpg, null, _.noop);
mJpegReqHandler._write(jpg, null, () => undefined);
}
mJpegReqHandler.close();
})
@@ -62,7 +61,7 @@ describe('MJpeg Stream (e2e)', function () {
});
it('should update mjpeg stream based on new data from mjpeg server', async function () {
stream = new MJpegStream(serverUrl, _.noop);
stream = new MJpegStream(serverUrl, () => undefined);
/* eslint-disable dot-notation -- access private lastChunk/updateCount for assertion */
expect(stream['lastChunk']).to.not.exist;
await stream.start();
@@ -94,7 +93,7 @@ describe('MJpeg Stream (e2e)', function () {
});
it('should error out if the server cannot be connected', async function () {
stream = new MJpegStream('http://localhost', _.noop);
stream = new MJpegStream('http://localhost', () => undefined);
await expect(stream.start()).to.eventually.be.rejected;
});
});
+2 -3
View File
@@ -4,7 +4,6 @@ import {fs, system, tempDir} from '../../lib';
import path from 'node:path';
import {createSandbox} from 'sinon';
import {exec} from 'teen_process';
import _ from 'lodash';
// TODO: normalize test organization
@@ -174,7 +173,7 @@ describe('fs', function () {
});
it('should walk all elements recursive', async function () {
await expect(
fs.walkDir(path.join(__dirname, '..', 'e2e', 'fixture'), true, _.noop)
fs.walkDir(path.join(__dirname, '..', 'e2e', 'fixture'), true, () => undefined)
).to.eventually.be.null;
});
it('should throw error through callback', async function () {
@@ -187,7 +186,7 @@ describe('fs', function () {
const filePath = await fs.walkDir(__dirname, false, (item) =>
item.endsWith('logger/helpers.js')
);
expect(_.isNil(filePath)).to.be.true;
expect(filePath).to.be.null;
});
});
+1 -2
View File
@@ -1,5 +1,4 @@
import sinon from 'sinon';
import _ from 'lodash';
import {logger} from '../../../lib';
let sandbox: sinon.SinonSandbox;
@@ -46,7 +45,7 @@ function someoneHadOutput(writers: ReturnType<typeof setupWriters>, output: stri
return !!(value && value.indexOf(output) >= 0);
}, 'matchOutput');
for (const writer of _.values(writers)) {
for (const writer of Object.values(writers)) {
if (writer.calledWithMatch) {
hadOutput = writer.calledWithMatch(matchOutput);
if (hadOutput) {
+1 -2
View File
@@ -1,7 +1,6 @@
import {expect} from 'chai';
import {node} from '../../lib';
import path from 'node:path';
import _ from 'lodash';
describe('node utilities', function () {
describe('getObjectSize', function () {
@@ -32,7 +31,7 @@ describe('node utilities', function () {
});
it('should return null if no root is found', function () {
expect(_.isNull(node.getModuleRootSync('yolo', __filename))).to.be.true;
expect(node.getModuleRootSync('yolo', __filename)).to.be.null;
});
});
+1 -1
View File
@@ -33,7 +33,7 @@ describe('npm', function () {
expect(npm.getLatestSafeUpgradeFromVersions('0.1.0', ['', '0.2.0'])).to.eql('0.2.0');
});
it('should return null if no newer version is found', function () {
expect(null === npm.getLatestSafeUpgradeFromVersions('10', versions1)).to.be.true;
expect(npm.getLatestSafeUpgradeFromVersions('10', versions1)).to.be.null;
});
});
});
+3 -4
View File
@@ -1,10 +1,9 @@
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {system} from '../../lib';
import {system, util} from '../../lib';
import os from 'node:os';
import {createSandbox} from 'sinon';
import * as teen_process from 'teen_process';
import _ from 'lodash';
const SANDBOX = Symbol();
const libs = {os, system};
@@ -83,7 +82,7 @@ describe('system', function () {
.returns({stdout: invalidOsx})
);
await expect(system.macOsxVersion()).to.eventually.be.rejectedWith(
new RegExp(_.escapeRegExp(invalidOsx))
new RegExp(util.escapeRegExp(invalidOsx))
);
});
});
@@ -91,7 +90,7 @@ describe('system', function () {
describe('architecture', function () {
beforeEach(function () {
mocks[SANDBOX] = sandbox;
for (const [key, value] of _.toPairs(libs)) {
for (const [key, value] of Object.entries(libs)) {
mocks[key] = sandbox.mock(value);
}
});
+2 -3
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {createSandbox} from 'sinon';
@@ -25,7 +24,7 @@ describe('timing', function () {
describe('bigint', function () {
beforeEach(function () {
if (!_.isFunction(process.hrtime.bigint)) {
if (typeof process.hrtime.bigint !== 'function') {
return this.skip();
}
processMock = sandbox.mock(process.hrtime);
@@ -50,7 +49,7 @@ describe('timing', function () {
const timer = new timing.Timer().start();
const duration = timer.getDuration();
expect(_.isNumber(duration.nanos)).to.be.true;
expect(duration.nanos).to.be.a('number');
});
it('should get correct seconds', function () {
setupMocks();
+229 -7
View File
@@ -5,7 +5,6 @@ import {sleep} from 'asyncbox';
import {createSandbox} from 'sinon';
import os from 'node:os';
import path from 'node:path';
import _ from 'lodash';
const {W3C_WEB_ELEMENT_IDENTIFIER} = util;
@@ -130,7 +129,7 @@ describe('util', function () {
describe('localIp', function () {
it('should find a local ip address', function () {
let ifConfigOut: any = {
const ifConfigOut: any = {
lo0: [
{
address: '::1',
@@ -186,7 +185,6 @@ describe('util', function () {
};
const osMock = sandbox.mock(os);
osMock.expects('networkInterfaces').returns(ifConfigOut);
ifConfigOut = '';
const ip = util.localIp();
expect(ip).to.eql('123.123.123.123');
osMock.verify();
@@ -224,7 +222,7 @@ describe('util', function () {
});
it('should pass null through', function () {
const obj = null;
expect(_.isNull(util.safeJsonParse(obj))).to.be.true;
expect(util.safeJsonParse(obj)).to.be.null;
});
it('should pass simple string through', function () {
const str = 'str';
@@ -257,7 +255,7 @@ describe('util', function () {
it('should use the replacer function on non-buffer values', function () {
const obj = {k1: 'v1', k2: 'v2', k3: 'v3'};
function replacer(_key: string, value: any) {
return _.isString(value) ? value.toUpperCase() : value;
return typeof value === 'string' ? value.toUpperCase() : value;
}
const jsonString = util.jsonStringify(obj, replacer);
expect(jsonString).to.include('V1');
@@ -271,7 +269,7 @@ describe('util', function () {
k3: Buffer.from('hi how are you today'),
};
function replacer(_key: string, value: any) {
return _.isString(value) ? value.toUpperCase() : value;
return typeof value === 'string' ? value.toUpperCase() : value;
}
const jsonString = util.jsonStringify(obj, replacer);
expect(jsonString).to.include('V1');
@@ -286,7 +284,7 @@ describe('util', function () {
k4: {k5: 'v5'},
};
function replacer(_key: string, value: any) {
return _.isString(value) ? value.toUpperCase() : value;
return typeof value === 'string' ? value.toUpperCase() : value;
}
const jsonString = util.jsonStringify(obj, replacer);
expect(jsonString).to.include('V1');
@@ -475,4 +473,228 @@ describe('util', function () {
expect(util.pluralize('word', 2, {inclusive: true})).to.eql('2 words');
});
});
describe('memoize', function () {
it('should memoize using first argument by default', function () {
let callCount = 0;
const fn = util.memoize((value: number) => {
callCount += 1;
return value * 2;
});
expect(fn(2)).to.equal(4);
expect(fn(2)).to.equal(4);
expect(callCount).to.equal(1);
});
it('should memoize by first argument only', function () {
let callCount = 0;
const fn = util.memoize((a: number, b: number) => {
callCount += 1;
return a + b;
});
expect(fn(1, 2)).to.equal(3);
expect(fn(1, 999)).to.equal(3);
expect(callCount).to.equal(1);
});
it('should memoize using a custom resolver', function () {
let callCount = 0;
const fn = util.memoize(
(a: number, b: number) => {
callCount += 1;
return a + b;
},
(_a, b) => b
);
expect(fn(1, 2)).to.equal(3);
expect(fn(999, 2)).to.equal(3);
expect(callCount).to.equal(1);
});
it('should use resolver keys to isolate cache entries', function () {
let callCount = 0;
const fn = util.memoize(
(value: number) => {
callCount += 1;
return value * 10;
},
(value) => value % 2
);
expect(fn(2)).to.equal(20);
expect(fn(4)).to.equal(20);
expect(fn(3)).to.equal(30);
expect(fn(5)).to.equal(30);
expect(callCount).to.equal(2);
});
it('should preserve this for resolver and wrapped function', function () {
const obj = {
prefix: 'ctx',
calls: 0,
fn: util.memoize(
function (this: {prefix: string; calls: number}, value: number) {
this.calls += 1;
return `${this.prefix}:${value}`;
},
function (this: {prefix: string}, value: number) {
return `${this.prefix}-${value}`;
}
),
};
expect(obj.fn(1)).to.equal('ctx:1');
expect(obj.fn(1)).to.equal('ctx:1');
expect(obj.calls).to.equal(1);
});
});
describe('isPlainObject', function () {
it('should return true for plain objects', function () {
expect(util.isPlainObject({})).to.be.true;
expect(util.isPlainObject(Object.create(null))).to.be.true;
});
it('should return false for non-plain objects', function () {
expect(util.isPlainObject([])).to.be.false;
expect(util.isPlainObject(new Date())).to.be.false;
expect(util.isPlainObject(null)).to.be.false;
});
it('should match lodash behavior for edge cases', function () {
const spoofed = {a: 1, [Symbol.toStringTag]: 'Custom'};
expect(util.isPlainObject(spoofed)).to.be.true;
function CustomCtor(this: any) {
this.a = 1;
}
const withCustomCtorOnProto = Object.create({constructor: CustomCtor});
expect(util.isPlainObject(withCustomCtorOnProto)).to.be.false;
});
});
describe('isEmpty', function () {
it('should handle strings and arrays', function () {
expect(util.isEmpty('')).to.be.true;
expect(util.isEmpty('x')).to.be.false;
expect(util.isEmpty([])).to.be.true;
expect(util.isEmpty([1])).to.be.false;
});
it('should handle objects and collections', function () {
expect(util.isEmpty({})).to.be.true;
expect(util.isEmpty({a: 1})).to.be.false;
expect(util.isEmpty(new Map())).to.be.true;
expect(util.isEmpty(new Set([1]))).to.be.false;
});
it('should handle non-plain objects with enumerable own properties', function () {
class Thing {}
const emptyInstance = new Thing();
const nonEmptyInstance = new Thing();
(nonEmptyInstance as unknown as {a?: number}).a = 1;
expect(util.isEmpty(emptyInstance)).to.be.true;
expect(util.isEmpty(nonEmptyInstance)).to.be.false;
const fn = () => undefined;
(fn as unknown as {x?: number}).x = 1;
expect(util.isEmpty(fn)).to.be.false;
});
});
describe('isEqual', function () {
it('should deeply compare nested objects', function () {
expect(util.isEqual({a: [1, {b: 'c'}]}, {a: [1, {b: 'c'}]})).to.be.true;
expect(util.isEqual({a: [1, {b: 'c'}]}, {a: [1, {b: 'd'}]})).to.be.false;
});
it('should compare special values and typed objects', function () {
expect(util.isEqual(NaN, NaN)).to.be.true;
expect(util.isEqual(new Date('2020-01-01'), new Date('2020-01-01'))).to.be.true;
expect(util.isEqual(/abc/gi, /abc/gi)).to.be.true;
expect(util.isEqual(Buffer.from('a'), Buffer.from('a'))).to.be.true;
});
it('should compare maps and sets', function () {
const entries: Array<[string, number | {c: number}]> = [
['a', 1],
['b', {c: 2}],
];
expect(util.isEqual(new Map(entries), new Map(entries))).to.be.true;
expect(util.isEqual(new Set([1, 2]), new Set([2, 1]))).to.be.true;
expect(util.isEqual(new Set([1, 2]), new Set([2, 3]))).to.be.false;
});
it('should compare functions by identity only', function () {
const fn1 = () => 1;
const fn2 = () => 1;
(fn1 as any).x = 1;
(fn2 as any).x = 1;
expect(util.isEqual(fn1, fn1)).to.be.true;
expect(util.isEqual(fn1, fn2)).to.be.false;
});
it('should ignore non-enumerable own properties', function () {
const left: Record<string, unknown> = {a: 1};
const right: Record<string, unknown> = {a: 1};
Object.defineProperty(left, 'hidden', {value: 1, enumerable: false});
Object.defineProperty(right, 'hidden', {value: 2, enumerable: false});
expect(util.isEqual(left, right)).to.be.true;
});
it('should compare errors and boxed symbols like lodash', function () {
expect(util.isEqual(new Error('boom'), new Error('boom'))).to.be.true;
expect(util.isEqual(new Error('boom'), new Error('kaboom'))).to.be.false;
expect(util.isEqual(Object(Symbol.for('x')), Object(Symbol.for('x')))).to.be.true;
expect(util.isEqual(Object(Symbol.for('x')), Object(Symbol.for('y')))).to.be.false;
});
});
describe('escapeRegExp', function () {
it('should escape regexp metacharacters', function () {
expect(util.escapeRegExp('a+b*c?.(x)[y]{z}|^$\\')).to.equal(
'a\\+b\\*c\\?\\.\\(x\\)\\[y\\]\\{z\\}\\|\\^\\$\\\\'
);
});
});
describe('uniq', function () {
it('should return a duplicate-free array preserving order', function () {
expect(util.uniq([1, 2, 1, 3, 2])).to.eql([1, 2, 3]);
});
});
describe('truncateString', function () {
it('should not change short strings', function () {
expect(util.truncateString('short')).to.equal('short');
});
it('should truncate with default options', function () {
const src = 'abcdefghijklmnopqrstuvwxyz0123456789';
expect(util.truncateString(src)).to.equal('abcdefghijklmnopqrstuvwxyz012…');
});
it('should support numeric length shorthand', function () {
expect(util.truncateString('abcdefghijklmnopqrstuvwxyz', 10)).to.equal('abcdefghi…');
});
it('should support custom omission', function () {
expect(util.truncateString('abcdefghijklmnopqrstuvwxyz', {length: 10, omission: '..'})).to.equal(
'abcdefgh..'
);
});
it('should handle non-string values safely', function () {
expect(() => util.truncateString(undefined as any)).not.to.throw();
expect(() => util.truncateString(null as any)).not.to.throw();
expect(util.truncateString(undefined as any)).to.equal('');
expect(util.truncateString(null as any)).to.equal('');
expect(util.truncateString(123456 as any, 5)).to.equal('1234…');
expect(util.truncateString({a: 1} as any, 8)).to.equal('[object…');
expect(util.truncateString(-0 as any)).to.equal('-0');
});
it('should return omission if max length is too small', function () {
expect(util.truncateString('hello world', 0)).to.equal('…');
});
});
});
+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.1.2](https://github.com/appium/appium/compare/@appium/test-support@4.1.1...@appium/test-support@4.1.2) (2026-05-06)
**Note:** Version bump only for package @appium/test-support
## [4.1.1](https://github.com/appium/appium/compare/@appium/test-support@4.1.0...@appium/test-support@4.1.1) (2026-04-23)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/test-support",
"version": "4.1.1",
"version": "4.1.2",
"description": "A collection of test utilities used across Appium packages",
"deprecated": "This module is deprecated. Use sinon directly instead (e.g., sinon.createSandbox() with Mocha hooks).",
"keywords": [
@@ -47,7 +47,7 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "7.1.1",
"@appium/support": "7.2.0",
"@colors/colors": "1.6.0",
"bluebird": "3.7.2",
"lodash": "4.18.1",
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.0](https://github.com/appium/appium/compare/@appium/types@1.3.1...@appium/types@1.4.0) (2026-05-06)
### Features
* add 3 WebDriver extension endpoints ([#22237](https://github.com/appium/appium/issues/22237)) ([787571d](https://github.com/appium/appium/commit/787571d33836c06bb1332d7bc6cc2f9800bbdd82))
## [1.3.1](https://github.com/appium/appium/compare/@appium/types@1.3.0...@appium/types@1.3.1) (2026-04-23)
@@ -296,6 +296,7 @@ export interface IOtherProtocolCommands {
/**
* Remove a specific auth credential
* @see {@link https://www.w3.org/TR/webauthn-2/#sctn-automation-remove-credential}
*
* @param credentialId - the credential ID
* @param authenticatorId - the authenticator ID
@@ -310,6 +311,39 @@ export interface IOtherProtocolCommands {
* @param authenticatorId - the authenticator id
*/
setUserAuthVerified?(isUserVerified: boolean, authenticatorId: string): Promise<void>;
/**
* Global Privacy Control (GPC)
*/
/**
* Set the do-not-sell-or-share preference value
* @see {@link https://www.w3.org/TR/gpc/#set-global-privacy-control}
*
* @param gpc - the value to set for the preference
*/
setGlobalPrivacyControl?(gpc: boolean): Promise<void>;
/**
* Get the do-not-sell-or-share preference value
* @see {@link https://www.w3.org/TR/gpc/#get-global-privacy-control}
*
* @returns the value of the preference
*/
getGlobalPrivacyControl?(): Promise<boolean>;
/**
* Storage Access
*/
/**
* Set the storage access policy
* @see {@link https://privacycg.github.io/storage-access/#set-storage-access-command}
*
* @param blocked - whether storage access should be blocked
* @param origin - the origin for which to set the policy
*/
setStorageAccess?(blocked: boolean, origin: string): Promise<void>;
}
// Permissions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/types",
"version": "1.3.1",
"version": "1.4.0",
"description": "Various type declarations used across Appium",
"keywords": [
"automation",
@@ -38,7 +38,7 @@
},
"dependencies": {
"@appium/logger": "2.0.7",
"@appium/schema": "1.1.0",
"@appium/schema": "1.1.1",
"@appium/tsconfig": "1.1.2",
"type-fest": "5.6.0"
},
@@ -47,13 +47,17 @@ async function main() {
ts = await compileFromFile(JSON_SCHEMA_PATH);
} catch (err) {
throw new Error(
`${error} Could not convert Appium schema JSON to TypeScript: ${err.message}. Does it exist?`
`${error} Could not convert Appium schema JSON to TypeScript: ${err.message}. Does it exist?`,
{cause: err}
);
}
try {
await fs.writeFile(OUTPUT_PATH, ts);
} catch (err) {
throw new Error(`${error} Could not write Appium schema declaration file: ${err.message}`);
throw new Error(
`${error} Could not write Appium schema declaration file: ${err.message}`,
{cause: err}
);
}
console.log(`${info} Wrote %s`, OUTPUT_PATH);
console.log(`${success} Done.`);
@@ -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.1.3](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@2.1.2...@appium/universal-xml-plugin@2.1.3) (2026-05-06)
**Note:** Version bump only for package @appium/universal-xml-plugin
## [2.1.2](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@2.1.1...@appium/universal-xml-plugin@2.1.2) (2026-04-23)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/universal-xml-plugin",
"version": "2.1.2",
"version": "2.1.3",
"description": "Appium plugin for making XML source and XPath queries the same across iOS and Android",
"keywords": [
"automation",
@@ -39,7 +39,7 @@
},
"dependencies": {
"@xmldom/xmldom": "0.9.10",
"fast-xml-parser": "5.7.1",
"fast-xml-parser": "5.7.2",
"lodash": "4.18.1",
"xpath": "0.0.34"
},