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