mirror of
https://github.com/appium/appium.git
synced 2026-10-31 22:32:24 +08:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c35776756 | ||
|
|
1b31db3be9 | ||
|
|
0a4b4f5a55 | ||
|
|
a085d44616 | ||
|
|
a10f558382 | ||
|
|
f61e7d57fa | ||
|
|
705710adc5 | ||
|
|
6a3d659c5a | ||
|
|
c760f02e14 | ||
|
|
e1ab96fc4e | ||
|
|
e8d31c63c4 | ||
|
|
787571d338 | ||
|
|
4849ad015d | ||
|
|
0878f3c8ce | ||
|
|
3dd0ac715a | ||
|
|
ce09eb2e7d | ||
|
|
9cc0377acf | ||
|
|
9b21b92418 | ||
|
|
c758fcaf13 | ||
|
|
63218cdac6 | ||
|
|
08493df3d1 | ||
|
|
bde5bd57f4 | ||
|
|
b5bdfdd4e4 | ||
|
|
cb4aa687a2 | ||
|
|
180f2611c1 | ||
|
|
1f3119ec1f | ||
|
|
2c0fb7a2e4 | ||
|
|
fa1e87535b | ||
|
|
79370cd307 | ||
|
|
712832ae61 | ||
|
|
6c9f7bbf6a | ||
|
|
0f0f60effd | ||
|
|
be35e0b3d1 |
@@ -13,4 +13,4 @@ jobs:
|
||||
name: Auto-Label PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fuxingloh/multi-labeler@v4
|
||||
- uses: fuxingloh/multi-labeler@v5
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Generated
+383
-485
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}. ` +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? '');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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,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;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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('…');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user