mirror of
https://github.com/appium/appium.git
synced 2026-11-01 06:45:05 +08:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7b20335ea | ||
|
|
8b9597b0e5 | ||
|
|
556e90be7e | ||
|
|
ab8ee41e79 | ||
|
|
028a4274e1 | ||
|
|
9a4e5c77ac | ||
|
|
cafb9bf089 | ||
|
|
cdfe8ddc48 | ||
|
|
5ff00a0a5c | ||
|
|
fec5f3a377 | ||
|
|
5e5b76cf6b | ||
|
|
efd13358cc | ||
|
|
17eb3cac86 | ||
|
|
4419664ffd | ||
|
|
4be29e49b5 | ||
|
|
f115ebed49 | ||
|
|
beb9da3320 | ||
|
|
7c9d1363a9 | ||
|
|
5e28714442 | ||
|
|
14507371f7 | ||
|
|
384afc77ff | ||
|
|
77c9b58989 | ||
|
|
9500bd17e3 | ||
|
|
1f9e786df2 | ||
|
|
a605032f2d | ||
|
|
bfa33c6bf7 | ||
|
|
99d663c930 | ||
|
|
70b383deaa | ||
|
|
43002e3286 | ||
|
|
2f949e581e | ||
|
|
27419248e4 | ||
|
|
a5bee22a07 | ||
|
|
cfaaf768a5 | ||
|
|
0508626518 | ||
|
|
9dc0030fb9 | ||
|
|
002f8fce7f | ||
|
|
57ae03f899 | ||
|
|
df5a7da467 | ||
|
|
8de688f020 | ||
|
|
d4fcd81aed | ||
|
|
aa90b9a840 | ||
|
|
1f6c82ce6d | ||
|
|
529aeb48f1 | ||
|
|
04d9520f76 | ||
|
|
6098bdeb92 | ||
|
|
a6c4fad26b | ||
|
|
8e36bfe56d | ||
|
|
fdb95389b1 | ||
|
|
30adda0092 | ||
|
|
441c4e5572 | ||
|
|
7235392585 | ||
|
|
ce91686852 | ||
|
|
cbf7e4dfd6 | ||
|
|
a9a66d7cf5 | ||
|
|
2bd2bb7667 | ||
|
|
56a45a5244 | ||
|
|
d9e4437c55 | ||
|
|
e9a6e5425c | ||
|
|
053a88bac1 | ||
|
|
cae84898d5 | ||
|
|
dcfaa88830 | ||
|
|
1143896b97 | ||
|
|
a90797683c | ||
|
|
cc42206b4d | ||
|
|
e9ca1f5dd1 | ||
|
|
a5ba1c5efb | ||
|
|
b0b219aa01 | ||
|
|
204a07f5a4 | ||
|
|
aeef6afb8b | ||
|
|
8093c2658c | ||
|
|
5b74a7653c | ||
|
|
671c37a89c | ||
|
|
cb2d6eef6c | ||
|
|
183a2683b6 | ||
|
|
ee3d935899 | ||
|
|
591e678e6f | ||
|
|
0cc3acf5e3 | ||
|
|
b20d36f979 | ||
|
|
146bc98876 | ||
|
|
477b837b1c | ||
|
|
ac78fedf43 | ||
|
|
b0e12852f0 | ||
|
|
274b28cb29 | ||
|
|
cb9324cda4 | ||
|
|
a4b47c02c6 | ||
|
|
adf90ee746 | ||
|
|
653b95965a | ||
|
|
18864bfcac | ||
|
|
6ae5930346 |
@@ -29,7 +29,7 @@ jobs:
|
||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_DOCS_PROJECT_ID }}
|
||||
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.9
|
||||
uses: peter-evans/create-pull-request@v8.1.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
+27
-21
@@ -1,24 +1,30 @@
|
||||
import tsConfig from '@appium/eslint-config-appium-ts';
|
||||
import appiumConfig from '@appium/eslint-config-appium-ts';
|
||||
import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
|
||||
export default [
|
||||
...tsConfig,
|
||||
{
|
||||
...tsConfig.find(({name}) => name === 'Test Files'),
|
||||
name: 'Test Support',
|
||||
files: [
|
||||
'packages/test-support/lib/**',
|
||||
'packages/driver-test-support/lib/**',
|
||||
'packages/plugin-test-support/lib/**',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ignores',
|
||||
ignores: [
|
||||
'**/build-fixtures/**',
|
||||
'packages/appium/docs/**/assets/**',
|
||||
'packages/appium/docs/**/js/**',
|
||||
'packages/appium/sample-code/**',
|
||||
],
|
||||
},
|
||||
// Create a modified config subset for test support files:
|
||||
// Extract the test file related configs (Mocha plugin and custom rules),
|
||||
// then override their 'files' property
|
||||
const testFileConfigItems = [appiumConfig[8], appiumConfig[9]];
|
||||
const testSupportFiles = [
|
||||
'packages/test-support/lib/**',
|
||||
'packages/driver-test-support/lib/**',
|
||||
'packages/plugin-test-support/lib/**',
|
||||
];
|
||||
const testSupportConfig = testFileConfigItems.map((item) => ({...item, files: testSupportFiles}));
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'Base Config',
|
||||
extends: [appiumConfig],
|
||||
},
|
||||
{
|
||||
name: 'Test Support',
|
||||
extends: [testSupportConfig],
|
||||
},
|
||||
globalIgnores([
|
||||
'**/build-fixtures/**',
|
||||
'packages/appium/docs/**/assets/**',
|
||||
'packages/appium/docs/**/js/**',
|
||||
'packages/appium/sample-code/**',
|
||||
]),
|
||||
]);
|
||||
|
||||
Generated
+1936
-16621
File diff suppressed because it is too large
Load Diff
+20
-21
@@ -85,10 +85,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@stylistic/eslint-plugin": "5.7.1",
|
||||
"@tsconfig/node20": "20.1.8",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/archiver": "7.0.0",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/base64-stream": "1.0.5",
|
||||
"@types/bluebird": "3.5.42",
|
||||
@@ -99,61 +100,59 @@
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/klaw": "3.0.7",
|
||||
"@types/lockfile": "1.0.4",
|
||||
"@types/lodash": "4.17.21",
|
||||
"@types/lodash": "4.17.23",
|
||||
"@types/method-override": "3.0.0",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/ncp": "2.0.8",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/node": "24.10.9",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@types/sinon": "21.0.0",
|
||||
"@types/sinon-chai": "4.0.0",
|
||||
"@types/source-map-support": "0.5.10",
|
||||
"@types/supports-color": "8.1.3",
|
||||
"@types/teen_process": "2.0.4",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.35",
|
||||
"asyncbox": "3.0.0",
|
||||
"chai": "6.2.1",
|
||||
"asyncbox": "6.0.1",
|
||||
"chai": "6.2.2",
|
||||
"chai-as-promised": "8.0.2",
|
||||
"conventional-changelog-conventionalcommits": "7.0.2",
|
||||
"cpy-cli": "6.0.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import-x": "4.16.1",
|
||||
"eslint-plugin-mocha": "11.2.0",
|
||||
"eslint-plugin-n": "17.23.2",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"finalhandler": "2.1.1",
|
||||
"get-port": "5.1.1",
|
||||
"get-port": "7.1.0",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "9.0.3",
|
||||
"log-symbols": "4.1.0",
|
||||
"log-symbols": "7.0.1",
|
||||
"midnight-smoker": "8.0.0",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
"mjpeg-server": "0.3.1",
|
||||
"mocha": "11.7.5",
|
||||
"npm-run-all2": "8.0.4",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.1",
|
||||
"rewiremock": "3.14.6",
|
||||
"rimraf": "6.1.2",
|
||||
"serve-static": "2.2.0",
|
||||
"sinon": "21.0.0",
|
||||
"serve-static": "2.2.1",
|
||||
"sinon": "21.0.1",
|
||||
"sinon-chai": "4.0.1",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"teen_process": "3.0.4",
|
||||
"teen_process": "4.0.8",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.33.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.48.1",
|
||||
"webdriverio": "9.21.0",
|
||||
"ws": "8.18.3",
|
||||
"typescript-eslint": "8.53.1",
|
||||
"webdriverio": "9.23.2",
|
||||
"ws": "8.19.0",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.2.0](https://github.com/appium/appium/compare/appium@3.1.2...appium@3.2.0) (2026-01-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Add repository links to verbose extensions list ([#21813](https://github.com/appium/appium/issues/21813)) ([ce91686](https://github.com/appium/appium/commit/ce91686852b0baab5f46990f5d40db4747922924))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** appium symlink behavior across drivers/plugins installations ([#21837](https://github.com/appium/appium/issues/21837)) ([529aeb4](https://github.com/appium/appium/commit/529aeb48f10419e21555cec05835ff07bb0e2e51))
|
||||
* apply npm run lint:fix ([#21867](https://github.com/appium/appium/issues/21867)) ([5e28714](https://github.com/appium/appium/commit/5e28714442cf59ee35b085e01e82c3f5483891fd))
|
||||
* **docs:** add missing verbose flag for CLI list command ([#21814](https://github.com/appium/appium/issues/21814)) ([dcfaa88](https://github.com/appium/appium/commit/dcfaa88830f6a5c608c92f5ae72445e065261d38))
|
||||
|
||||
|
||||
|
||||
## [3.1.2](https://github.com/appium/appium/compare/appium@3.1.1...appium@3.1.2) (2025-12-04)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
@@ -19,6 +19,36 @@ Appium has a graphical client which can be used to inspect application screensho
|
||||
application hierarchy, search for elements, run Appium commands, record app interactions, and more.
|
||||
It can be very useful for Appium test development.
|
||||
|
||||
=== "Install as an Appium Plugin"
|
||||
|
||||
```bash
|
||||
appium plugin install inspector
|
||||
```
|
||||
=== "Install Standalone"
|
||||
|
||||
Download the app from the Appium Inspector repo's [GitHub Releases section](https://github.com/appium/appium-inspector/releases).
|
||||
|
||||
### [Appium MCP](https://github.com/appium/appium-mcp)
|
||||
|
||||
Appium MCP is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"appium-mcp": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["appium-mcp@latest"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Tools
|
||||
|
||||
Appium driver or plugin developers can choose to include these tools in their driver/plugin:
|
||||
@@ -33,7 +63,7 @@ details needed for the driver/plugin have been set up correctly. The tool can be
|
||||
appium {driver|plugin} doctor <extension-name>
|
||||
```
|
||||
|
||||
It shows no results if the driver/plugin author did not implement Doctor support.
|
||||
It shows no results if the driver/plugin does not have such validation checks.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -55,29 +85,3 @@ Supported by: `@AppiumTestDistribution`
|
||||
```sh title="Install This Tool"
|
||||
npm install -g appium-installer
|
||||
```
|
||||
|
||||
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
|
||||
|
||||
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
Supported by: `@AppiumTestDistribution`
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"mcpServers": {
|
||||
"jarvis-appium": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["jarvis-appium"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk",
|
||||
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -134,6 +134,7 @@ appium {driver|plugin} list
|
||||
|`--installed`|Only list all installed extensions|boolean|
|
||||
|`--json`|Return the result in JSON format|boolean|
|
||||
|`--updates`|List all extensions along with information on whether newer versions are available. Only supported for extensions installed via `npm`.|boolean|
|
||||
|`--verbose`|Show additional details for each extension|boolean|
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ Hub](https://opencollective.com/appium).
|
||||
We are very grateful for the support of our Development Partners, who donate Appium development and
|
||||
maintenance as part of their employees' core job duties!
|
||||
|
||||
<a href="https://lambdatest.com/?utm_source=appium.io&utm_medium=organic&utm_campaign=june_25&utm_term=sk&utm_content=webpage">
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-dark.png#only-dark" width="250" alt="LambdaTest" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="250" alt="LambdaTest" />
|
||||
</a>
|
||||
|
||||
## Strategic Partners
|
||||
|
||||
We are very grateful for the financial and marketing support of our exclusive Strategic Partners,
|
||||
|
||||
@@ -21,6 +21,41 @@ Appium has a graphical client which can be used to inspect application screensho
|
||||
application hierarchy, search for elements, run Appium commands, record app interactions, and more.
|
||||
It can be very useful for Appium test development.
|
||||
|
||||
\=== "Install as an Appium Plugin"
|
||||
|
||||
````
|
||||
```bash
|
||||
appium plugin install inspector
|
||||
```
|
||||
````
|
||||
|
||||
\=== "Install Standalone"
|
||||
|
||||
```
|
||||
Download the app from the Appium Inspector repo's [GitHub Releases section](https://github.com/appium/appium-inspector/releases).
|
||||
```
|
||||
|
||||
### [Appium MCP](https://github.com/appium/appium-mcp)
|
||||
|
||||
Appium MCP is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"appium-mcp": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["appium-mcp@latest"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Tools
|
||||
|
||||
Appium driver or plugin developers can choose to include these tools in their driver/plugin:
|
||||
@@ -35,7 +70,7 @@ details needed for the driver/plugin have been set up correctly. The tool can be
|
||||
appium {driver|plugin} doctor <extension-name>
|
||||
```
|
||||
|
||||
It shows no results if the driver/plugin author did not implement Doctor support.
|
||||
It shows no results if the driver/plugin does not have such validation checks.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -59,29 +94,3 @@ Supported by: `@AppiumTestDistribution`
|
||||
```sh title="Install This Tool"
|
||||
npm install -g appium-installer
|
||||
```
|
||||
|
||||
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
|
||||
|
||||
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
Supported by: `@AppiumTestDistribution`
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"mcpServers": {
|
||||
"jarvis-appium": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["jarvis-appium"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk",
|
||||
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -135,6 +135,7 @@ appium {driver|plugin} list
|
||||
| `--installed` | Only list all installed extensions | boolean |
|
||||
| `--json` | Return the result in JSON format | boolean |
|
||||
| `--updates` | List all extensions along with information on whether newer versions are available. Only supported for extensions installed via `npm`. | boolean |
|
||||
| `--verbose` | Show additional details for each extension | boolean |
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ Hub.
|
||||
We are very grateful for the support of our Development Partners, who donate Appium development and
|
||||
maintenance as part of their employees' core job duties!
|
||||
|
||||
<a href="https://lambdatest.com/?utm_source=appium.io&utm_medium=organic&utm_campaign=june_25&utm_term=sk&utm_content=webpage">
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-dark.png#only-dark" width="250" alt="LambdaTest" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="250" alt="LambdaTest" />
|
||||
</a>
|
||||
|
||||
## Strategic Partners
|
||||
|
||||
We are very grateful for the financial and marketing support of our exclusive Strategic Partners,
|
||||
|
||||
@@ -9,40 +9,40 @@ theme:
|
||||
nav:
|
||||
- 欢迎使用: index.md
|
||||
- 博客: blog/index.md
|
||||
- 简介:
|
||||
- 简介:
|
||||
- intro/index.md
|
||||
- 背景:
|
||||
- 背景:
|
||||
- intro/appium.md
|
||||
- intro/drivers.md
|
||||
- intro/clients.md
|
||||
- intro/history.md
|
||||
- 快速开始:
|
||||
- 快速开始:
|
||||
- quickstart/index.md
|
||||
- quickstart/requirements.md
|
||||
- quickstart/install.md
|
||||
- quickstart/uiauto2-driver.md
|
||||
- 写一个测试:
|
||||
- 写一个测试:
|
||||
- quickstart/test-js.md
|
||||
- quickstart/test-py.md
|
||||
- quickstart/test-java.md
|
||||
- quickstart/test-rb.md
|
||||
- quickstart/test-dotnet.md
|
||||
- quickstart/next-steps.md
|
||||
- 生态系统:
|
||||
- 生态系统:
|
||||
- ecosystem/index.md
|
||||
- ecosystem/drivers.md
|
||||
- ecosystem/clients.md
|
||||
- ecosystem/plugins.md
|
||||
- ecosystem/tools.md
|
||||
- 参考:
|
||||
- 参考:
|
||||
- reference/index.md
|
||||
- 命令行界面:
|
||||
- 命令行界面:
|
||||
- reference/cli/index.md
|
||||
- reference/cli/server.md
|
||||
- reference/cli/extensions.md
|
||||
- reference/cli/setup.md
|
||||
- reference/cli/env-vars.md
|
||||
- API 端点:
|
||||
- API 端点:
|
||||
- reference/api/index.md
|
||||
- reference/api/webdriver.md
|
||||
- reference/api/bidi.md
|
||||
@@ -51,11 +51,11 @@ nav:
|
||||
- reference/api/appium.md
|
||||
- reference/api/others.md
|
||||
- reference/api/plugins.md
|
||||
- 指南:
|
||||
- 迁移:
|
||||
- 指南:
|
||||
- 迁移:
|
||||
- guides/migrating-2-to-3.md
|
||||
- guides/migrating-1-to-2.md
|
||||
- 服务器/驱动程序配置:
|
||||
- 服务器/驱动程序配置:
|
||||
- guides/managing-exts.md
|
||||
- guides/branch-testing.md
|
||||
- guides/config.md
|
||||
@@ -65,24 +65,24 @@ nav:
|
||||
- guides/grid.md
|
||||
- guides/caching.md
|
||||
- guides/tls.md
|
||||
- 会话配置:
|
||||
- 会话配置:
|
||||
- guides/caps.md
|
||||
- guides/settings.md
|
||||
- guides/execute-methods.md
|
||||
- guides/context.md
|
||||
- guides/event-timing.md
|
||||
- 更多资源:
|
||||
- 更多资源:
|
||||
- resources/index.md
|
||||
- 开发:
|
||||
- 开发:
|
||||
- developing/index.md
|
||||
- developing/build-drivers.md
|
||||
- developing/build-plugins.md
|
||||
- developing/build-docs.md
|
||||
- developing/build-doctor-checks.md
|
||||
- developing/sensitive.md
|
||||
- 开发者参考:
|
||||
- 开发者参考:
|
||||
- developing/config-system.md
|
||||
- 贡献:
|
||||
- 贡献:
|
||||
- contributing/index.md
|
||||
- 赞助者:
|
||||
- 赞助者:
|
||||
- sponsors/index.md
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block announce %}
|
||||
<center>
|
||||
SeleniumConf & AppiumConf returns on May 6-8, 2026! Join us to learn and connect with the community!
|
||||
<a href="https://seleniumconf.com/">Register Now</a>
|
||||
</center>
|
||||
{% endblock %}
|
||||
@@ -20,7 +20,7 @@ const {
|
||||
DOCS_REMOTE,
|
||||
LATEST_ALIAS,
|
||||
} = require('./utils');
|
||||
const path = require('path');
|
||||
const path = require('node:path');
|
||||
const semver = require('semver');
|
||||
const {version} = require('../../package.json');
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||
/* eslint-disable promise/prefer-await-to-then */
|
||||
|
||||
const path = require('path');
|
||||
const path = require('node:path');
|
||||
const {fs} = require('@appium/support');
|
||||
const {AppiumConfigJsonSchema} = require('@appium/schema');
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// for simplicity this file is not transpiled and is run directly via an npm script
|
||||
//
|
||||
const {logger} = require('@appium/support');
|
||||
const path = require('path');
|
||||
const path = require('node:path');
|
||||
|
||||
const log = logger.getLogger('Docs');
|
||||
|
||||
|
||||
@@ -21,6 +21,41 @@ Appium has a graphical client which can be used to inspect application screensho
|
||||
application hierarchy, search for elements, run Appium commands, record app interactions, and more.
|
||||
It can be very useful for Appium test development.
|
||||
|
||||
\=== "Install as an Appium Plugin"
|
||||
|
||||
````
|
||||
```bash
|
||||
appium plugin install inspector
|
||||
```
|
||||
````
|
||||
|
||||
\=== "Install Standalone"
|
||||
|
||||
```
|
||||
Download the app from the Appium Inspector repo's [GitHub Releases section](https://github.com/appium/appium-inspector/releases).
|
||||
```
|
||||
|
||||
### [Appium MCP](https://github.com/appium/appium-mcp)
|
||||
|
||||
Appium MCP is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"appium-mcp": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["appium-mcp@latest"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extension Tools
|
||||
|
||||
Appium driver or plugin developers can choose to include these tools in their driver/plugin:
|
||||
@@ -35,7 +70,7 @@ details needed for the driver/plugin have been set up correctly. The tool can be
|
||||
appium {driver|plugin} doctor <extension-name>
|
||||
```
|
||||
|
||||
It shows no results if the driver/plugin author did not implement Doctor support.
|
||||
It shows no results if the driver/plugin does not have such validation checks.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -59,29 +94,3 @@ Supported by: `@AppiumTestDistribution`
|
||||
```sh title="Install This Tool"
|
||||
npm install -g appium-installer
|
||||
```
|
||||
|
||||
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
|
||||
|
||||
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
|
||||
automation capabilities using Appium.
|
||||
|
||||
Supported by: `@AppiumTestDistribution`
|
||||
|
||||
```json title="Install This Tool"
|
||||
// Add the following to your MCP client configuration
|
||||
{
|
||||
"mcpServers": {
|
||||
"jarvis-appium": {
|
||||
"disabled": false,
|
||||
"timeout": 100,
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["jarvis-appium"],
|
||||
"env": {
|
||||
"ANDROID_HOME": "/path/to/android/sdk",
|
||||
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
---
|
||||
title: Intro to Appium Drivers
|
||||
title: Appium 驱动程序介绍
|
||||
---
|
||||
|
||||
As the [main Overview](./appium.md) makes clear, "drivers" are basically Appium's answer to the
|
||||
question, "how do we support automation of multiple, unrelated platforms?" In this doc we'll get
|
||||
into a little more detail about how drivers work. The specific details of how drivers work probably
|
||||
don't matter too much for you, unless you're planning on writing your own driver or contributing to
|
||||
an existing driver (things we hope you do!).
|
||||
正如[概览](./appium.md)所阐明的那样,"驱动程序"基本上是 Appium 对"我们如何支持多个不相关平台的自动化?"这一问题的答案。 在本文档中,我们将更详细地了解驱动程序的工作原理。 驱动程序工作的具体细节对你来说可能并不那么重要,除非你打算编写自己的驱动程序或为现有驱动程序做贡献(我们希望你这样做!)。
|
||||
|
||||
The main benefit in understanding a bit more of how drivers work is that being aware of the typical
|
||||
complexity or the typical driver architecture will inform your debugging process when you
|
||||
inevitably run into an issue in one of your tests.
|
||||
了解更多驱动程序工作原理的主要好处是,了解典型的复杂性或典型的驱动程序架构将有助于你在测试中遇到问题时进行调试。
|
||||
|
||||
## Interface Implementations
|
||||
## 接口实现
|
||||
|
||||
At the most basic level, drivers are simply Node.js classes that extend a special class included in
|
||||
Appium, called `BaseDriver`. You could have something very close to a "working" driver, with these
|
||||
very simple lines of code:
|
||||
在最基本的层面上,驱动程序只是扩展了 Appium 中包含的一个特殊类的 Node.js 类,这个类叫做 `BaseDriver`。 你可以用以下非常简单的几行代码就拥有一个非常接近"可工作"的驱动程序:
|
||||
|
||||
```js
|
||||
import BaseDriver from '@appium/base-driver'
|
||||
@@ -25,21 +17,11 @@ class MyNewDriver extends BaseDriver {
|
||||
}
|
||||
```
|
||||
|
||||
This empty driver doesn't _do_ anything, but you could wrap it up in a Node.js module, add a few
|
||||
Appium-related fields to the module's manifest (`package.json`), and then install it using `appium
|
||||
driver install`.
|
||||
这个空驱动程序不会_做_任何事情,但你可以将它打包成一个 Node.js 模块,在模块的清单文件(`package.json`)中添加一些与 Appium 相关的字段,然后使用 `appium driver install` 安装它。
|
||||
|
||||
So, from a technical perspective, an Appium driver is just a bit of code that inherits from some
|
||||
other Appium code. That's it! Now, inheriting from `BaseDriver` actually gives us a lot, because
|
||||
`BaseDriver` is essentially an encapsulation of the entire WebDriver protocol. So all a driver
|
||||
needs to do something useful is to _implement_ Node.js methods with names corresponding to
|
||||
their WebDriver protocol equivalents.
|
||||
因此,从技术角度来看,Appium 驱动程序只是一些继承自其他 Appium 代码的代码。 就是这样! 现在,继承自 `BaseDriver` 实际上给了我们很多东西,因为 `BaseDriver` 本质上是整个 WebDriver 协议的封装。 所以驱动程序要做一些有用的事情,只需要_实现_名称与其 WebDriver 协议等效的 Node.js 方法。
|
||||
|
||||
So let's say I wanted to do something with this empty driver; first I have to decide which
|
||||
WebDriver command I want to implement. For our example, let's take the Navigate
|
||||
To WebDriver command. Leave aside for the moment
|
||||
what I want to have the driver _do_ when this command is executed. To tell Appium the driver can
|
||||
handle the command, all we have to do is define a method like this in our driver class:[^1]
|
||||
假设我想用这个空驱动程序做点什么;首先我必须决定要实现哪个 WebDriver 命令。 对于我们的例子,让我们选择[Navigate To](https://w3c.github.io/webdriver/#navigate-to) WebDriver 命令。 暂时不考虑当这个命令执行时我想让驱动程序_做_什么。 要告诉 Appium 驱动程序可以处理这个命令,我们所要做的就是在驱动程序类中定义一个像这样的方法:[^1]
|
||||
|
||||
```js
|
||||
async setUrl(url) {
|
||||
@@ -47,142 +29,73 @@ async setUrl(url) {
|
||||
}
|
||||
```
|
||||
|
||||
[^1]: You might notice that `setUrl` doesn't look anything like `Navigate To`, so how did we know
|
||||
to use it rather than some other random string? Well, Appium's WebDriver-protocol-to-method-name
|
||||
mapping is defined in a special file within the `@appium/base-driver` package called
|
||||
[routes.js](https://github.com/appium/appium/blob/master/packages/base-driver/lib/protocol/routes.js).
|
||||
So if you're writing a driver, this is where you would go to figure out what method names to use
|
||||
and what parameters to expect. Or you could look at the source for any of the main Appium
|
||||
drivers!
|
||||
[^1]: 你可能会注意到 `setUrl` 看起来与 `Navigate To` 完全不同,那么我们怎么知道使用它而不是其他随机字符串呢? 其实,Appium 的 WebDriver 协议到方法名的映射是在 `@appium/base-driver` 包中的一个特殊文件中定义的,叫做 [routes.js](https://github.com/appium/appium/blob/master/packages/base-driver/lib/protocol/routes.js)。
|
||||
所以如果你正在编写驱动程序,这就是你要去找出要使用什么方法名和期望什么参数的地方。 或者你可以查看任何主要 Appium 驱动程序的源代码!
|
||||
|
||||
That's it! How we actually implement the command is totally up to us, and depends on the
|
||||
platform(s) we want to support. Here are some different example implementations of this command for
|
||||
different platforms:
|
||||
就是这样! 我们如何实际实现该命令完全取决于我们自己,并取决于我们想要支持的平台。 以下是针对不同平台的此命令的一些不同示例实现:
|
||||
|
||||
- Browsers: execute some JavaScript to set `window.location.href`
|
||||
- iOS apps: launch an app using a deep link
|
||||
- Android apps: launch an app using a deep link
|
||||
- React apps: load a specific route
|
||||
- Unity: go to a named scene
|
||||
- 浏览器:执行一些 JavaScript 来设置 `window.location.href`
|
||||
- iOS 应用:使用深度链接启动应用
|
||||
- Android 应用:使用深度链接启动应用
|
||||
- React 应用:加载特定路由
|
||||
- Unity:转到指定场景
|
||||
|
||||
So you can see there can be a lot of differences between how drivers implement the same WebDriver
|
||||
command across platforms.[^2] What is the _same_, though, is how they express that they can handle
|
||||
a protocol command.
|
||||
因此,你可以看到,不同平台上驱动程序实现相同 WebDriver 命令的方式可能存在很大差异。[^2] 然而,_相同_的是它们表达可以处理协议命令的方式。
|
||||
|
||||
[^2]: Of course, we want to keep the semantics as similar as possible, but in the world of iOS, for
|
||||
example, launching an app via a deep link (a URL with a special app-specific scheme) is about as
|
||||
close as we are going to get to navigating to a web URL.
|
||||
[^2]: 当然,我们希望保持语义尽可能相似,但在 iOS 的世界里,例如,通过深度链接(具有特殊应用特定方案的 URL)启动应用是我们最接近导航到 Web URL 的方式。
|
||||
|
||||
We're going into this great amount of detail (which you don't need to remember, by the way),
|
||||
because it's important to stress the point that an Appium driver is not inherently anything in
|
||||
particular, other than a bit of JS code that can handle WebDriver protocol commands. Where you go
|
||||
from there is up to you, the driver author!
|
||||
我们之所以如此详细地讨论(顺便说一下,你不需要记住这些),是因为重要的是要强调一点,Appium 驱动程序本质上不是任何特定的东西,只是一些可以处理 WebDriver 协议命令的 JS 代码。 从那里去哪里取决于你,驱动程序作者!
|
||||
|
||||
## Automation mapping
|
||||
## 自动化映射
|
||||
|
||||
But _typically_ what driver authors want to do is to provide automation behaviours for a given
|
||||
platform(s) that are semantically very similar to the the WebDriver spec implementations for
|
||||
browsers. When you want to find an element, you should get a reference to a UI element. When you
|
||||
want to click or tap that element, the resulting behaviour should be the same as if a person were
|
||||
to click or tap on the element. And so on.
|
||||
但_通常_驱动程序作者想要做的是为给定的平台提供自动化行为,这些行为在语义上与浏览器的 WebDriver 规范实现非常相似。 当你想查找一个元素时,你应该得到一个 UI 元素的引用。 当你想点击或轻触该元素时,产生的行为应该与人点击或轻触该元素相同。 等等。
|
||||
|
||||
So the real challenge for driver authors is not how to work with the WebDriver protocol (because
|
||||
`BaseDriver` encapsulates all that for you), but how to make the actual automation happen on the
|
||||
target platform. Every driver relies on its own set of underlying technologies here. As mentioned
|
||||
in the [Overview](index.md), the iOS driver uses an Apple technology called
|
||||
[XCUITest](https://developer.apple.com/documentation/xctest/xcuielement). These underlying
|
||||
automation technologies usually have proprietary or idiosyncratic APIs of their own. Writing
|
||||
a driver becomes the task of mapping the WebDriver protocol to this underlying API (or sometimes
|
||||
a set of different underlying APIs--for example, the UiAutomator2 driver relies not only on the
|
||||
[UiAutomator2](https://developer.android.com/training/testing/other-components/ui-automator)
|
||||
technology from Google, but also functions only available through
|
||||
[ADB](https://developer.android.com/tools/adb), as well as functions only available
|
||||
via the Android SDK inside a helper app). Tying it all together into a single, usable, WebDriver
|
||||
interface is the incredibly useful (but incredibly challenging) art of driver development!
|
||||
因此,驱动程序作者面临的真正挑战不是如何使用 WebDriver 协议(因为 `BaseDriver` 为你封装了所有这些),而是如何在目标平台上实现实际的自动化。 每个驱动程序都依赖于自己的一套底层技术。 正如[概览](index.md)中提到的,iOS 驱动程序使用 Apple 的一项名为 [XCUITest](https://developer.apple.com/documentation/xctest/xcuielement) 的技术。 这些底层自动化技术通常都有自己专有或特殊的 API。 编写驱动程序就是将 WebDriver 协议映射到这个底层 API 的任务(有时是一组不同的底层 API——例如,UiAutomator2 驱动程序不仅依赖于 Google 的 [UiAutomator2](https://developer.android.com/training/testing/other-components/ui-automator) 技术,还依赖于只能通过 [ADB](https://developer.android.com/tools/adb) 获得的功能,以及只能通过辅助应用内的 Android SDK 获得的功能)。 将所有这些整合到一个单一、可用的 WebDriver 接口中,是驱动程序开发这项非常有用(但极具挑战性)的艺术!
|
||||
|
||||
## Multi-level architecture
|
||||
## 多层架构
|
||||
|
||||
In practice, this often results in a pretty complex architecture. Let's take iOS for example again.
|
||||
The XCUITest framework (the one used by the Appium driver) expects code that calls it to be written
|
||||
in Objective-C or Swift. Furthermore, XCUITest code can only be run in a special mode triggered by
|
||||
Xcode (and directly or indirectly, the Xcode command line tools). In other words, there's no
|
||||
straightforward way to go from a Node.js function implementation (like `setUrl()` above) to
|
||||
XCUITest API calls.
|
||||
在实践中,这通常会导致相当复杂的架构。 让我们再次以 iOS 为例。
|
||||
XCUITest 框架(Appium 驱动程序使用的框架)期望调用它的代码用 Objective-C 或 Swift 编写。 此外,XCUITest 代码只能在由 Xcode(以及直接或间接的 Xcode 命令行工具)触发的特殊模式下运行。 换句话说,没有直接的方法从 Node.js 函数实现(如上面的 `setUrl()`)到 XCUITest API 调用。
|
||||
|
||||
What the XCUITest driver authors have done is instead to split the driver into two parts: one part
|
||||
written in Node.js (the part which is incorporated into Appium and which initially handles the
|
||||
WebDriver commands), and the other part written in Objective-C (the part which actually gets run on
|
||||
an iOS device and makes XCUITest API calls). This makes interfacing with XCUITest possible, but
|
||||
introduces the new problem of coordination between the two parts.
|
||||
XCUITest 驱动程序作者所做的是将驱动程序分成两部分:一部分用 Node.js 编写(这部分被整合到 Appium 中并最初处理 WebDriver 命令),另一部分用 Objective-C 编写(这部分实际在 iOS 设备上运行并进行 XCUITest API 调用)。 这使得与 XCUITest 的接口成为可能,但引入了两部分之间协调的新问题。
|
||||
|
||||
The driver authors could have chosen any of a number of very different strategies to model the
|
||||
communication between the Node.js side and the Objective-C side, but at the end of the day decided
|
||||
to use ... the WebDriver protocol! That's right, the Objective-C side of the XCUITest driver is
|
||||
itself a WebDriver implementation, called
|
||||
[WebDriverAgent](https://github.com/appium/webdriveragent).[^3]
|
||||
驱动程序作者本可以选择许多非常不同的策略来建模 Node.js 端和 Objective-C 端之间的通信,但最终决定使用……
|
||||
WebDriver 协议! 没错,XCUITest 驱动程序的 Objective-C 端本身就是一个 WebDriver 实现,称为 [WebDriverAgent](https://github.com/appium/webdriveragent)。[^3]
|
||||
|
||||
[^3]: You could in theory, therefore, point your WebDriver client straight to WebDriverAgent and
|
||||
bypass Appium entirely. This is usually not convenient, however, for a few reasons:
|
||||
[^3]: 因此,理论上你可以将你的 WebDriver 客户端直接指向 WebDriverAgent 并完全绕过 Appium。 然而,这通常不方便,原因如下:
|
||||
|
||||
- The Appium XCUITest driver builds and manages WebDriverAgent for you, which can be a pain and
|
||||
involves the use of Xcode.
|
||||
- The XCUITest driver does lots more than what can be done by WebDriverAgent, for example working
|
||||
with simulators or devices, installing apps, and the like.
|
||||
- Appium XCUITest 驱动程序会为你构建和管理 WebDriverAgent,这可能很麻烦并涉及使用 Xcode。
|
||||
- XCUITest 驱动程序做的事情远不止 WebDriverAgent 所能做的,例如使用模拟器或设备、安装应用等。
|
||||
|
||||
The moral of the story is that driver architectures can become quite complicated and multilayered,
|
||||
due to the nature of the problem we're trying to solve. It also means it can be difficult sometimes
|
||||
to tell where in this chain of technologies something has gone wrong, if you run into a problem
|
||||
with a particular test. With the XCUITest world again, we have something like the following set of
|
||||
technologies all in play at the same time:
|
||||
这个故事的寓意是,由于我们试图解决的问题的性质,驱动程序架构可能会变得相当复杂和多层。 这也意味着,如果你在特定测试中遇到问题,有时很难判断这一系列技术中的哪个环节出了问题。 再以 XCUITest 为例,我们同时使用以下一组技术:
|
||||
|
||||
- Your test code (in its programming language) - owned by you
|
||||
- The Appium client library - owned by Appium
|
||||
- The Selenium client library - owned by Selenium
|
||||
- The network (local or Internet)
|
||||
- The Appium server - owned by Appium
|
||||
- The Appium XCUITest driver - owned by Appium
|
||||
- WebDriverAgent - owned by Appium
|
||||
- Xcode - owned by Apple
|
||||
- XCUITest - owned by Apple
|
||||
- iOS itself - owned by Apple
|
||||
- macOS (where Xcode and iOS simulators run) - owned by Apple
|
||||
- 你的测试代码(使用其编程语言)- 由你拥有
|
||||
- Appium 客户端库 - 由 Appium 拥有
|
||||
- Selenium 客户端库 - 由 Selenium 拥有
|
||||
- 网络(本地或互联网)
|
||||
- Appium 服务器 - 由 Appium 拥有
|
||||
- Appium XCUITest 驱动程序 - 由 Appium 拥有
|
||||
- WebDriverAgent - 由 Appium 拥有
|
||||
- Xcode - 由 Apple 拥有
|
||||
- XCUITest - 由 Apple 拥有
|
||||
- iOS 本身 - 由 Apple 拥有
|
||||
- macOS(运行 Xcode 和 iOS 模拟器的地方)- 由 Apple 拥有
|
||||
|
||||
It's a pretty deep stack!
|
||||
这是一个相当深的技术栈!
|
||||
|
||||
## Proxy mode
|
||||
## 代理模式
|
||||
|
||||
There's one other important architectural aspect of drivers to understand. It can be exemplified
|
||||
again by the XCUITest driver. Recall that we just discussed how the two "halves" of the XCUITest
|
||||
driver both speak the WebDriver protocol---the Node.js half clicks right into Appium's WebDriver
|
||||
server, and the Objective-c half (WebDriverAgent) is its own WebDriver implementation.
|
||||
还有一个关于驱动程序的重要架构方面需要了解。 可以再次用 XCUITest 驱动程序来举例说明。 回想一下我们刚才讨论的 XCUITest 驱动程序的两个"部分"是如何都使用 WebDriver 协议的——Node.js 部分直接连接到 Appium 的 WebDriver 服务器,而 Objective-C 部分(WebDriverAgent)本身就是一个 WebDriver 实现。
|
||||
|
||||
This opens up the possibility of Appium taking a shortcut in certain cases. Let's imagine that the
|
||||
XCUITest driver needs to implement the `Click Element` command. The internal code of this
|
||||
implementation would look something like taking the appropriate parameters and constructing an HTTP
|
||||
request to the WebDriverAgent server. In this case, we're basically just reconstructing the
|
||||
client's original call to the Appium server\![^4] So there's really no need to even write a function
|
||||
implementing the `Click Element` command. Instead, the XCUITest driver can just let Appium know
|
||||
that this command should be proxied directly to some other WebDriver server.
|
||||
这为 Appium 在某些情况下走捷径开辟了可能性。
|
||||
让我们想象一下 XCUITest 驱动程序需要实现 `Click Element` 命令。 这个实现的内部代码看起来就像是获取适当的参数并构造一个到 WebDriverAgent 服务器的 HTTP 请求。 在这种情况下,我们基本上只是在重建客户端对 Appium 服务器的原始调用![^4] 所以实际上没有必要编写一个函数来实现 `Click Element` 命令。 相反,XCUITest 驱动程序可以让 Appium 知道这个命令应该直接代理到其他 WebDriver 服务器。
|
||||
|
||||
[^4]: It's not _exactly_ the same call, because the Appium server and the WebDriverAgent server
|
||||
will generate different session IDs, but these differences will be handled transparently.
|
||||
[^4]: 这不是_完全_相同的调用,因为 Appium 服务器和 WebDriverAgent 服务器会生成不同的会话 ID,但这些差异会被透明地处理。
|
||||
|
||||
If you're not familiar with the concept of "proxying," in this case it just means that the XCUITest
|
||||
driver will not be involved at all in handling the command. Instead it will merely be repackaged
|
||||
and forwarded to WebDriverAgent at the protocol level, and WebDriverAgent's response will likewise
|
||||
be passed back directly to the client, without any XCUITest driver code seeing it or modifying it.
|
||||
如果你不熟悉"代理"的概念,在这种情况下,它只是意味着 XCUITest 驱动程序根本不会参与处理该命令。 相反,它只会在协议级别被重新打包并转发到 WebDriverAgent,WebDriverAgent 的响应同样会直接传回客户端,而不会有任何 XCUITest 驱动程序代码看到或修改它。
|
||||
|
||||
This architectural pattern provides a nice bonus for driver authors who choose to deal with the
|
||||
WebDriver protocol everywhere, rather than constructing bespoke protocols. It also means that
|
||||
Appium can create wrapper drivers for any other existing WebDriver implementation very easily. If
|
||||
you look at the [Appium Safari driver](https://github.com/appium/appium-safari-driver) code, for
|
||||
example, you'll see that it implements basically no standard commands, because all of these are
|
||||
proxied directly to an underlying SafariDriver process.
|
||||
这种架构模式为选择在各处使用 WebDriver 协议而不是构建定制协议的驱动程序作者提供了一个很好的额外好处。 这也意味着 Appium 可以非常轻松地为任何其他现有的 WebDriver 实现创建包装驱动程序。 例如,如果你查看 [Appium Safari 驱动程序](https://github.com/appium/appium-safari-driver)代码,你会发现它基本上没有实现任何标准命令,因为所有这些命令都直接代理到底层的 SafariDriver 进程。
|
||||
|
||||
It's important to understand that this proxying business is sometimes happening under the hood,
|
||||
because if you're ever diving into some open source driver code trying to figure out where
|
||||
a command is implemented, you might be surprised to find no implementation at all in the Node.js
|
||||
driver code itself! In that case, you'll need to figure out where commands are being proxied to so
|
||||
you can look there for the appropriate implementation.
|
||||
了解这种代理机制有时在幕后发生是很重要的,因为如果你曾经深入研究一些开源驱动程序代码试图找出命令的实现位置,你可能会惊讶地发现 Node.js 驱动程序代码本身根本没有任何实现! 在这种情况下,你需要找出命令被代理到哪里,以便你可以在那里查找相应的实现。
|
||||
|
||||
OK, that's enough for this very detailed introduction to drivers!
|
||||
好了,对于这个非常详细的驱动程序介绍来说,这就足够了!
|
||||
|
||||
@@ -2,38 +2,35 @@
|
||||
hide:
|
||||
- toc
|
||||
|
||||
title: Appium in a Nutshell
|
||||
title: Appium 概览
|
||||
---
|
||||
|
||||
As mentioned on the main page, Appium aims to support UI automation of many _different platforms_
|
||||
(mobile, web, desktop, etc.). Not only that, but it also aims to support automation code written in
|
||||
_different languages_ (JS, Java, Python, etc.). Combining all of this functionality in a single
|
||||
program is a very daunting, if not impossible task!
|
||||
如主页所述,Appium 旨在支持_多个不同平台_(移动端、Web、桌面等)的 UI 自动化。 不仅如此,它还旨在支持用_不同语言_(JS、Java、Python 等)编写的自动化代码。 将所有这些功能整合到一个程序中是一项非常艰巨的任务,甚至可以说是不可能完成的任务!
|
||||
|
||||
In order to achieve this, Appium is effectively split into four parts:
|
||||
为了实现这一目标,Appium 实际上被分为四个部分:
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-image-filter-center-focus-strong: **Appium Core** - defines the core APIs
|
||||
- :material-car: **Drivers** - implement connectivity to specific platforms
|
||||
- :octicons-code-16: **Clients** - implement Appium's API in specific languages
|
||||
- :fontawesome-solid-plug: **Plugins** - change or extend Appium's core functionality
|
||||
- :material-image-filter-center-focus-strong: **Appium 核心** - 定义核心 API
|
||||
- :material-car: **驱动程序** - 实现与特定平台的连接
|
||||
- :octicons-code-16: **客户端库** - 用特定语言实现 Appium 的 API
|
||||
- :fontawesome-solid-plug: **插件** - 改变或扩展 Appium 的核心功能
|
||||
|
||||
</div>
|
||||
|
||||
Therefore, in order to start automating something with Appium, you need to:
|
||||
因此,要开始使用 Appium 进行自动化,你需要:
|
||||
|
||||
- Install Appium itself
|
||||
- Install a driver for your target platform
|
||||
- Install a client library for your target programming language
|
||||
- (Optional) install one or more plugins
|
||||
- 安装 Appium 本身
|
||||
- 为你的目标平台安装驱动程序
|
||||
- 为你的目标编程语言安装客户端库
|
||||
- (可选)安装一个或多个插件
|
||||
|
||||
These are the basics! If you are ready to jump in, proceed with the [Quickstart](../quickstart/index.md)!
|
||||
这些就是基础知识! 如果你已经准备好开始,请继续阅读[快速入门](../quickstart/index.md)!
|
||||
|
||||
If you wish to learn more details about how it all works, see these pages for background material:
|
||||
如果你想了解更多关于其工作原理的详细信息,请参阅以下页面的背景材料:
|
||||
|
||||
- [Appium Core](./appium.md)
|
||||
- [Appium Drivers](./drivers.md)
|
||||
- [Appium Clients](./clients.md)
|
||||
- [Appium 核心](./appium.md)
|
||||
- [Appium 驱动程序](./drivers.md)
|
||||
- [Appium 客户端](./clients.md)
|
||||
|
||||
Finally, to learn about the origins of Appium, check out the [Appium Project History](./history.md).
|
||||
最后,要了解 Appium 的起源,请查看 [Appium 项目历史](./history.md)。
|
||||
|
||||
@@ -135,6 +135,7 @@ appium {driver|plugin} list
|
||||
| `--installed` | Only list all installed extensions | boolean |
|
||||
| `--json` | Return the result in JSON format | boolean |
|
||||
| `--updates` | List all extensions along with information on whether newer versions are available. Only supported for extensions installed via `npm`. | boolean |
|
||||
| `--verbose` | Show additional details for each extension | boolean |
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ Hub.
|
||||
We are very grateful for the support of our Development Partners, who donate Appium development and
|
||||
maintenance as part of their employees' core job duties!
|
||||
|
||||
<a href="https://lambdatest.com/?utm_source=appium.io&utm_medium=organic&utm_campaign=june_25&utm_term=sk&utm_content=webpage">
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-dark.png#only-dark" width="250" alt="LambdaTest" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="250" alt="LambdaTest" />
|
||||
</a>
|
||||
|
||||
## Strategic Partners
|
||||
|
||||
We are very grateful for the financial and marketing support of our exclusive Strategic Partners,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {asyncify} = require('asyncbox');
|
||||
|
||||
const appium = require('./build/lib/main.js');
|
||||
|
||||
if (require.main === module) {
|
||||
asyncify(appium.main);
|
||||
appium.main();
|
||||
}
|
||||
|
||||
module.exports = appium;
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
BiDiResultData
|
||||
} from '@appium/types';
|
||||
|
||||
type ExtensionPlugin = Plugin & ExtensionCore
|
||||
type ExtensionPlugin = Plugin & ExtensionCore;
|
||||
type AnyDriver = ExternalDriver | AppiumDriver;
|
||||
type SendData = (data: string | Buffer) => Promise<void>;
|
||||
type LogSocketError = (err: Error) => void;
|
||||
@@ -448,7 +448,7 @@ function initBidiEventListeners(
|
||||
// sure the client is subscribed and then pass it on
|
||||
const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
|
||||
BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
|
||||
const eventListenerFactory = (extType: 'driver'|'plugin', ext: ExtensionCore) => {
|
||||
const eventListenerFactory = (extType: 'driver' | 'plugin', ext: ExtensionCore) => {
|
||||
const eventListener = async ({context, method, params = {}}) => {
|
||||
// if the driver didn't specify a context, use the empty context
|
||||
if (!context) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {npm, util, env, console, fs, system} from '@appium/support';
|
||||
import {spinWith, RingBuffer} from './utils';
|
||||
import {
|
||||
@@ -12,14 +12,15 @@ import {
|
||||
} from '../extension/extension-config';
|
||||
import {SubProcess} from 'teen_process';
|
||||
import {packageDidChange} from '../extension/package-changed';
|
||||
import {spawn} from 'child_process';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {inspect} from 'node:util';
|
||||
import {pathToFileURL} from 'url';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
|
||||
import {getAppiumModuleRoot, npmPackage} from '../utils';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const UPDATE_ALL = 'installed';
|
||||
const MAX_CONCURRENT_REPO_FETCHES = 5;
|
||||
|
||||
class NotUpdatableError extends Error {}
|
||||
class NoUpdatesAvailableError extends Error {}
|
||||
@@ -96,6 +97,7 @@ class ExtensionCliCommand {
|
||||
* For TS to understand that a function throws an exception, it must actually throw an exception--
|
||||
* in other words, _calling_ a function which is guaranteed to throw an exception is not enough--
|
||||
* nor is something like `@returns {never}` which does not imply a thrown exception.
|
||||
*
|
||||
* @param {string} message
|
||||
* @protected
|
||||
* @throws {Error}
|
||||
@@ -121,18 +123,45 @@ class ExtensionCliCommand {
|
||||
|
||||
/**
|
||||
* List extensions
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ListOptions} opts
|
||||
* @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
|
||||
*/
|
||||
async list({showInstalled, showUpdates, verbose = false}) {
|
||||
let lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
|
||||
if (verbose) {
|
||||
lsMsg += ' (verbose mode)';
|
||||
const listData = this._buildListData(showInstalled);
|
||||
|
||||
const lsMsg =
|
||||
`Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s` +
|
||||
(verbose ? ' (verbose mode)' : ' (rerun with --verbose for more info)');
|
||||
await this._checkForUpdates(listData, showUpdates, lsMsg);
|
||||
|
||||
if (this.isJsonOutput) {
|
||||
await this._addRepositoryUrlsToListData(listData);
|
||||
return listData;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
await this._addRepositoryUrlsToListData(listData);
|
||||
this.log.log(inspect(listData, {colors: true, depth: null}));
|
||||
return listData;
|
||||
}
|
||||
|
||||
return await this._displayNormalListOutput(listData, showUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the initial list data structure from installed and known extensions
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {boolean} showInstalled
|
||||
* @returns {ExtensionList<ExtType>}
|
||||
* @private
|
||||
*/
|
||||
_buildListData(showInstalled) {
|
||||
const installedNames = Object.keys(this.config.installedExtensions);
|
||||
const knownNames = Object.keys(this.knownExtensions);
|
||||
const listData = [...installedNames, ...knownNames].reduce((acc, name) => {
|
||||
return [...installedNames, ...knownNames].reduce((acc, name) => {
|
||||
if (!acc[name]) {
|
||||
if (installedNames.includes(name)) {
|
||||
acc[name] = {
|
||||
@@ -148,101 +177,235 @@ class ExtensionCliCommand {
|
||||
}
|
||||
return acc;
|
||||
}, /** @type {ExtensionList<ExtType>} */ ({}));
|
||||
}
|
||||
|
||||
// if we want to show whether updates are available, put that behind a spinner
|
||||
/**
|
||||
* Check for available updates for installed extensions
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtensionList<ExtType>} listData
|
||||
* @param {boolean} showUpdates
|
||||
* @param {string} lsMsg
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _checkForUpdates(listData, showUpdates, lsMsg) {
|
||||
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
||||
// We'd like to still show lsMsg even if showUpdates is false
|
||||
if (!showUpdates) {
|
||||
return;
|
||||
}
|
||||
for (const [ext, data] of _.toPairs(listData)) {
|
||||
if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
|
||||
// don't need to check for updates on exts that aren't installed
|
||||
// also don't need to check for updates on non-npm exts
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const updates = await this.checkForExtensionUpdate(ext);
|
||||
data.updateVersion = updates.safeUpdate;
|
||||
data.unsafeUpdateVersion = updates.unsafeUpdate;
|
||||
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
|
||||
} catch (e) {
|
||||
data.updateError = e.message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Type guard to narrow "installed" extensions, which have more data
|
||||
* @param {any} data
|
||||
* @returns {data is InstalledExtensionListData<ExtType>}
|
||||
*/
|
||||
const extIsInstalled = (data) => Boolean(data.installed);
|
||||
// Filter to only extensions that need update checks (installed npm packages)
|
||||
const extensionsToCheck = _.toPairs(listData).filter(
|
||||
([, data]) => data.installed && data.installType === INSTALL_TYPE_NPM
|
||||
);
|
||||
|
||||
// if we're just getting the data, short circuit return here since we don't need to do any
|
||||
// formatting logic
|
||||
if (this.isJsonOutput) {
|
||||
return listData;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
this.log.log(inspect(listData, {colors: true, depth: null}));
|
||||
return listData;
|
||||
}
|
||||
for (const [name, data] of _.toPairs(listData)) {
|
||||
let installTxt = ' [not installed]'.grey;
|
||||
let updateTxt = '';
|
||||
let upToDateTxt = '';
|
||||
let unsafeUpdateTxt = '';
|
||||
if (extIsInstalled(data)) {
|
||||
const {
|
||||
installType,
|
||||
installSpec,
|
||||
updateVersion,
|
||||
unsafeUpdateVersion,
|
||||
version,
|
||||
upToDate,
|
||||
updateError,
|
||||
} = data;
|
||||
let typeTxt;
|
||||
switch (installType) {
|
||||
case INSTALL_TYPE_GIT:
|
||||
case INSTALL_TYPE_GITHUB:
|
||||
typeTxt = `(cloned from ${installSpec})`.yellow;
|
||||
break;
|
||||
case INSTALL_TYPE_LOCAL:
|
||||
typeTxt = `(linked from ${installSpec})`.magenta;
|
||||
break;
|
||||
case INSTALL_TYPE_DEV:
|
||||
typeTxt = '(dev mode)';
|
||||
break;
|
||||
default:
|
||||
typeTxt = '(npm)';
|
||||
}
|
||||
installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
|
||||
|
||||
if (showUpdates) {
|
||||
if (updateError) {
|
||||
updateTxt = ` [Cannot check for updates: ${updateError}]`.red;
|
||||
} else {
|
||||
if (updateVersion) {
|
||||
updateTxt = ` [${updateVersion} available]`.magenta;
|
||||
}
|
||||
if (upToDate) {
|
||||
upToDateTxt = ` [Up to date]`.green;
|
||||
}
|
||||
if (unsafeUpdateVersion) {
|
||||
unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
|
||||
}
|
||||
await B.map(
|
||||
extensionsToCheck,
|
||||
async ([ext, data]) => {
|
||||
try {
|
||||
const updates = await this.checkForExtensionUpdate(ext);
|
||||
data.updateVersion = updates.safeUpdate;
|
||||
data.unsafeUpdateVersion = updates.unsafeUpdate;
|
||||
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
|
||||
} catch (e) {
|
||||
data.updateError = e.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.log.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
|
||||
/**
|
||||
* Add repository URLs to list data for all extensions
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtensionList<ExtType>} listData
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _addRepositoryUrlsToListData(listData) {
|
||||
await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
|
||||
await B.map(
|
||||
_.values(listData),
|
||||
async (data) => {
|
||||
const repoUrl = await this._getRepositoryUrl(data);
|
||||
if (repoUrl) {
|
||||
data.repositoryUrl = repoUrl;
|
||||
}
|
||||
},
|
||||
{concurrency: MAX_CONCURRENT_REPO_FETCHES}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display normal formatted output
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtensionList<ExtType>} listData
|
||||
* @param {boolean} showUpdates
|
||||
* @returns {Promise<ExtensionList<ExtType>>}
|
||||
* @private
|
||||
*/
|
||||
async _displayNormalListOutput(listData, showUpdates) {
|
||||
for (const [name, data] of _.toPairs(listData)) {
|
||||
const line = await this._formatExtensionLine(name, data, showUpdates);
|
||||
this.log.log(line);
|
||||
}
|
||||
|
||||
return listData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single extension line for display
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {string} name
|
||||
* @param {ExtensionListData<ExtType>} data
|
||||
* @param {boolean} showUpdates
|
||||
* @returns {Promise<string>}
|
||||
* @private
|
||||
*/
|
||||
async _formatExtensionLine(name, data, showUpdates) {
|
||||
if (data.installed) {
|
||||
const installTxt = this._formatInstallText(/** @type {InstalledExtensionListData<ExtType>} */ (data));
|
||||
const updateTxt = showUpdates ? this._formatUpdateText(/** @type {InstalledExtensionListData<ExtType>} */ (data)) : '';
|
||||
return `- ${name.yellow}${installTxt}${updateTxt}`;
|
||||
}
|
||||
const installTxt = ' [not installed]'.grey;
|
||||
return `- ${name.yellow}${installTxt}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format installation status text
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {InstalledExtensionListData<ExtType>} data
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_formatInstallText(data) {
|
||||
const {installType, installSpec, version} = data;
|
||||
let typeTxt;
|
||||
switch (installType) {
|
||||
case INSTALL_TYPE_GIT:
|
||||
case INSTALL_TYPE_GITHUB:
|
||||
typeTxt = `(cloned from ${installSpec})`.yellow;
|
||||
break;
|
||||
case INSTALL_TYPE_LOCAL:
|
||||
typeTxt = `(linked from ${installSpec})`.magenta;
|
||||
break;
|
||||
case INSTALL_TYPE_DEV:
|
||||
typeTxt = '(dev mode)';
|
||||
break;
|
||||
default:
|
||||
typeTxt = '(npm)';
|
||||
}
|
||||
return `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format update information text
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {InstalledExtensionListData<ExtType>} data
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_formatUpdateText(data) {
|
||||
const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
|
||||
if (updateError) {
|
||||
return ` [Cannot check for updates: ${updateError}]`.red;
|
||||
}
|
||||
let txt = '';
|
||||
if (updateVersion) {
|
||||
txt += ` [${updateVersion} available]`.magenta;
|
||||
}
|
||||
if (upToDate) {
|
||||
txt += ` [Up to date]`.green;
|
||||
}
|
||||
if (unsafeUpdateVersion) {
|
||||
txt += ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
|
||||
}
|
||||
return txt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository URL from package data
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtensionListData<ExtType>} data
|
||||
* @returns {Promise<string|null>}
|
||||
* @private
|
||||
*/
|
||||
async _getRepositoryUrl(data) {
|
||||
if (data.installed && data.installPath) {
|
||||
return await this._getRepositoryUrlFromInstalled(
|
||||
/** @type {InstalledExtensionListData<ExtType>} */ (data)
|
||||
);
|
||||
}
|
||||
if (data.pkgName && !data.installed) {
|
||||
return await this._getRepositoryUrlFromNpm(data.pkgName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository URL from installed extension's package.json
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {InstalledExtensionListData<ExtType>} data
|
||||
* @returns {Promise<string|null>}
|
||||
* @private
|
||||
*/
|
||||
async _getRepositoryUrlFromInstalled(data) {
|
||||
try {
|
||||
const pkgJsonPath = path.join(data.installPath, 'package.json');
|
||||
if (await fs.exists(pkgJsonPath)) {
|
||||
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
||||
if (pkg.repository) {
|
||||
if (typeof pkg.repository === 'string') {
|
||||
return pkg.repository;
|
||||
}
|
||||
if (pkg.repository.url) {
|
||||
return pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading package.json
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository URL from npm for a package name
|
||||
*
|
||||
* @param {string} pkgName
|
||||
* @returns {Promise<string|null>}
|
||||
* @private
|
||||
*/
|
||||
async _getRepositoryUrlFromNpm(pkgName) {
|
||||
try {
|
||||
const repoInfo = await npm.getPackageInfo(pkgName, ['repository']);
|
||||
// When requesting only 'repository', npm.getPackageInfo returns the repository object directly
|
||||
if (repoInfo) {
|
||||
if (typeof repoInfo === 'string') {
|
||||
return repoInfo;
|
||||
}
|
||||
if (repoInfo.url) {
|
||||
return repoInfo.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors fetching from npm
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given extension is compatible with the currently installed server
|
||||
*
|
||||
@@ -454,18 +617,6 @@ class ExtensionCliCommand {
|
||||
return {pkg, installPath};
|
||||
});
|
||||
|
||||
/** @type {Promise<void>[]} */
|
||||
const symlinkInjectionPromises = _.uniq([
|
||||
...Object.values(this.config.installedExtensions).map((ext) => ext.installPath),
|
||||
installPath,
|
||||
]).map((instPath) => injectAppiumSymlink.bind(this)(path.join(instPath, 'node_modules')));
|
||||
// After the extension is installed, we try to inject the appium module symlink
|
||||
// into the extension's node_modules folder if it is not there yet.
|
||||
// We also inject the symlink into other installed extensions' node_modules folders
|
||||
// as these might be cleaned up unexpectedly by npm
|
||||
// (see https://github.com/appium/python-client/pull/1177#issuecomment-3419826643).
|
||||
await Promise.all(symlinkInjectionPromises);
|
||||
|
||||
return this.getInstallationReceipt({
|
||||
pkg,
|
||||
installPath,
|
||||
@@ -971,11 +1122,35 @@ class ExtensionCliCommand {
|
||||
* This is needed to ensure proper module resolution for installed extensions,
|
||||
* especially ESM ones.
|
||||
*
|
||||
* @this {ExtensionCliCommand}
|
||||
* @param {ExtensionConfig<ExtensionType>} driverConfig
|
||||
* @param {ExtensionConfig<ExtensionType>} pluginConfig
|
||||
* @param {import('@appium/types').AppiumLogger} logger
|
||||
*/
|
||||
export async function injectAppiumSymlinks(driverConfig, pluginConfig, logger) {
|
||||
const installPaths = _.compact([
|
||||
...Object.values(driverConfig.installedExtensions || {}),
|
||||
...Object.values(pluginConfig.installedExtensions || {})
|
||||
].filter((details) => details.installType === INSTALL_TYPE_NPM)
|
||||
.map((details) => details.installPath));
|
||||
// After the extension is installed, we try to inject the appium module symlink
|
||||
// into the extension's node_modules folder if it is not there yet.
|
||||
// We also inject the symlink into other installed extensions' node_modules folders
|
||||
// as these might be cleaned up unexpectedly by npm
|
||||
// (see https://github.com/appium/python-client/pull/1177#issuecomment-3419826643).
|
||||
await Promise.all(
|
||||
installPaths.map((installPath) => injectAppiumSymlink(path.join(installPath, 'node_modules'), logger))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is needed to ensure proper module resolution for installed extensions,
|
||||
* especially ESM ones.
|
||||
*
|
||||
* @param {string} dstFolder The destination folder where the symlink should be created
|
||||
* @param {import('@appium/types').AppiumLogger} logger
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function injectAppiumSymlink(dstFolder) {
|
||||
async function injectAppiumSymlink(dstFolder, logger) {
|
||||
let appiumModuleRoot;
|
||||
try {
|
||||
appiumModuleRoot = getAppiumModuleRoot();
|
||||
@@ -985,7 +1160,7 @@ async function injectAppiumSymlink(dstFolder) {
|
||||
}
|
||||
} catch (error) {
|
||||
// This error is not fatal, we may still doing just fine if the module being loaded is a CJS one
|
||||
this.log.info(
|
||||
logger.info(
|
||||
`Cannot create a symlink to the appium module '${appiumModuleRoot}' in '${dstFolder}'. ` +
|
||||
`Original error: ${error.message}`
|
||||
);
|
||||
@@ -1013,6 +1188,7 @@ export {ExtensionCliCommand as ExtensionCommand};
|
||||
* @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
|
||||
* @property {string} [updateError] - Update check error message (if present)
|
||||
* @property {boolean} [devMode] - If Appium is run from an extension's working copy
|
||||
* @property {string} [repositoryUrl] - Repository URL for the extension (if available)
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {fs} from '@appium/support';
|
||||
import {ArgumentParser} from 'argparse';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_DOCTOR,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* The name of the extension type for drivers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {util, fs, system} from '@appium/support';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {satisfies} from 'semver';
|
||||
import {commandClasses} from '../cli/extension';
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
isAllowedSchemaFileExtension,
|
||||
registerSchema,
|
||||
} from '../schema/schema';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const DEFAULT_ENTRY_POINT = 'index.js';
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import B from 'bluebird';
|
||||
import {env, fs} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import * as YAML from 'yaml';
|
||||
import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import {INSTALL_TYPE_NPM, INSTALL_TYPE_DEV} from './extension-config';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {fs} from '@appium/support';
|
||||
import {isPackageChanged} from 'package-changed';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {PKG_HASHFILE_RELATIVE_PATH} from '../constants';
|
||||
import log from '../logger';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
normalizeBasePath,
|
||||
} from '@appium/base-driver';
|
||||
import {util, env} from '@appium/support';
|
||||
import {asyncify} from 'asyncbox';
|
||||
import _ from 'lodash';
|
||||
import {AppiumDriver} from './appium';
|
||||
import {runExtensionCommand} from './cli/extension';
|
||||
@@ -43,6 +42,7 @@ import {
|
||||
isBroadcastIp,
|
||||
} from './utils';
|
||||
import net from 'node:net';
|
||||
import { injectAppiumSymlinks } from './cli/extension-command';
|
||||
|
||||
const {resolveAppiumHome} = env;
|
||||
/*
|
||||
@@ -314,6 +314,14 @@ async function init(args) {
|
||||
if (isPluginCommandArgs(preConfigArgs)) {
|
||||
await runExtensionCommand(preConfigArgs, pluginConfig);
|
||||
}
|
||||
|
||||
if (isDriverCommandArgs(preConfigArgs) || isPluginCommandArgs(preConfigArgs)) {
|
||||
// @ts-ignore The linter suggest using dot
|
||||
const cmd = preConfigArgs.driverCommand || preConfigArgs.pluginCommand;
|
||||
if (cmd === 'install') {
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
return /** @type {InitResult<Cmd>} */ ({});
|
||||
}
|
||||
@@ -481,7 +489,7 @@ async function main(args) {
|
||||
// (more specifically, `build/lib/main.js`)
|
||||
// the executable is now `../index.js`, so that module will typically be `require.main`.
|
||||
if (require.main === module) {
|
||||
asyncify(main);
|
||||
main();
|
||||
}
|
||||
|
||||
// everything below here is intended to be a public API.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ArgumentTypeError} from 'argparse';
|
||||
import {readFileSync, existsSync} from 'fs';
|
||||
import {readFileSync, existsSync} from 'node:fs';
|
||||
|
||||
/**
|
||||
* This module provides custom keywords for Appium schemas, as well as
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Ajv} from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import {AppiumConfigJsonSchema} from '@appium/schema';
|
||||
import {APPIUM_CONFIG_SCHEMA_ID, ArgSpec, SERVER_PROP_NAME} from './arg-spec';
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
errors,
|
||||
isW3cCaps,
|
||||
} from '@appium/base-driver';
|
||||
import {inspect as dump} from 'util';
|
||||
import {inspect as dump} from 'node:util';
|
||||
import {node, fs} from '@appium/support';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {SERVER_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE, SETUP_SUBCOMMAND} from './constants';
|
||||
import os from 'node:os';
|
||||
|
||||
@@ -229,7 +229,7 @@ export function adjustNodePath() {
|
||||
// See https://gist.github.com/branneman/8048520#7-the-hack
|
||||
// @ts-ignore see above comment
|
||||
|
||||
require('module').Module._initPaths();
|
||||
require('node:module').Module._initPaths();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "3.1.2",
|
||||
"version": "3.2.0",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,7 +40,8 @@
|
||||
"scripts/autoinstall-extensions.js",
|
||||
"types",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test"
|
||||
],
|
||||
"scripts": {
|
||||
"build:docs": "node docs/scripts/build-docs.js",
|
||||
@@ -60,34 +61,31 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.1.2",
|
||||
"@appium/base-plugin": "^3.0.5",
|
||||
"@appium/docutils": "^2.2.0",
|
||||
"@appium/logger": "^2.0.3",
|
||||
"@appium/schema": "^1.0.0",
|
||||
"@appium/support": "^7.0.4",
|
||||
"@appium/types": "^1.1.2",
|
||||
"@appium/base-driver": "^10.2.0",
|
||||
"@appium/base-plugin": "^3.0.6",
|
||||
"@appium/docutils": "^2.2.1",
|
||||
"@appium/logger": "^2.0.4",
|
||||
"@appium/schema": "^1.0.1",
|
||||
"@appium/support": "^7.0.5",
|
||||
"@appium/types": "^1.2.0",
|
||||
"@sidvind/better-ajv-errors": "4.0.1",
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.13.2",
|
||||
"axios": "1.13.3",
|
||||
"bluebird": "3.7.2",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.2.4",
|
||||
"lodash": "4.17.23",
|
||||
"lru-cache": "11.2.5",
|
||||
"ora": "5.4.1",
|
||||
"package-changed": "3.0.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.7.3",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "3.0.4",
|
||||
"type-fest": "5.3.0",
|
||||
"winston": "3.18.3",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.3",
|
||||
"teen_process": "4.0.8",
|
||||
"type-fest": "5.4.1",
|
||||
"winston": "3.19.0",
|
||||
"ws": "8.19.0",
|
||||
"yaml": "2.8.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "9.21.0"
|
||||
"webdriverio": "9.23.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,6 @@ let PLUGIN_TYPE;
|
||||
/** @type {typeof import('../lib/extension').loadExtensions} */
|
||||
let loadExtensions;
|
||||
|
||||
const _ = require('lodash');
|
||||
const wrap = _.partial(
|
||||
require('wrap-ansi'),
|
||||
_,
|
||||
process.stderr.columns ?? process.stdout.columns ?? 80
|
||||
);
|
||||
const ora = require('ora');
|
||||
|
||||
/** @type {typeof import('@appium/support').env} */
|
||||
@@ -54,7 +48,7 @@ let util;
|
||||
let logger;
|
||||
|
||||
function log(message) {
|
||||
console.error(wrap(`[Appium] ${message}`));
|
||||
console.error(`[Appium] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,9 +112,12 @@ async function main() {
|
||||
}).start();
|
||||
|
||||
if (!driverEnv && !pluginEnv) {
|
||||
spinner.succeed(
|
||||
wrap(`No drivers or plugins to automatically install.
|
||||
If desired, provide arguments with comma-separated values "--drivers=<known_driver>[,known_driver...]" and/or "--plugins=<known_plugin>[,known_plugin...]" to the "npm install appium" command. The specified extensions will be installed automatically with Appium. Note: to see the list of known extensions, run "appium <driver|plugin> list".`)
|
||||
spinner.succeed('No drivers or plugins to automatically install.');
|
||||
log(
|
||||
'If desired, provide the argument "--drivers=<driver_name>[,<driver_name>...]" and/or ' +
|
||||
'"--plugins=<plugin_name>[,<plugin_name>...]" to the "npm install appium" command. ' +
|
||||
'The specified extensions will be installed automatically alongside Appium. ' +
|
||||
'For a list of known extensions, run "appium <driver|plugin> list".'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
const childProcess = require('child_process');
|
||||
const path = require('node:path');
|
||||
const childProcess = require('node:child_process');
|
||||
const _ = require('lodash');
|
||||
|
||||
const res = JSON.parse(
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
const path = require('path');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const {fs, util, logger} = require('@appium/support');
|
||||
const Handlebars = require('handlebars');
|
||||
const _ = require('lodash');
|
||||
const {asyncify} = require('asyncbox');
|
||||
const url = require('url');
|
||||
|
||||
const log = logger.getLogger('YamlParser');
|
||||
|
||||
@@ -192,7 +190,7 @@ Handlebars.registerHelper('if_eq', function ifEq(a, b, opts) {
|
||||
});
|
||||
|
||||
function getBaseHostname(fullUrl) {
|
||||
const baseUrl = url.parse(fullUrl);
|
||||
const baseUrl = new URL(fullUrl);
|
||||
return baseUrl.hostname;
|
||||
}
|
||||
|
||||
@@ -418,4 +416,4 @@ async function main() {
|
||||
await generateCommandIndex();
|
||||
}
|
||||
|
||||
asyncify(main);
|
||||
main();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {exec} from 'teen_process';
|
||||
import {fs, system, tempDir, util} from '@appium/support';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
@@ -98,10 +98,12 @@ describe('Driver CLI', function () {
|
||||
it('should list available drivers in json format', async function () {
|
||||
const driverData = await runList();
|
||||
for (const d of Object.keys(KNOWN_DRIVERS)) {
|
||||
driverData[d].should.eql({
|
||||
installed: false,
|
||||
pkgName: KNOWN_DRIVERS[d],
|
||||
});
|
||||
driverData[d].should.have.property('installed', false);
|
||||
driverData[d].should.have.property('pkgName', KNOWN_DRIVERS[d]);
|
||||
// repositoryUrl may be present if available
|
||||
if (driverData[d].repositoryUrl) {
|
||||
driverData[d].repositoryUrl.should.be.a('string');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -179,6 +181,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.uiautomator2.installed;
|
||||
// @ts-ignore
|
||||
delete list.uiautomator2.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
@@ -190,6 +194,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
// @ts-ignore
|
||||
delete list.fake.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
@@ -223,6 +229,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
// @ts-ignore
|
||||
delete list.fake.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
@@ -245,6 +253,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
// @ts-ignore
|
||||
delete list.fake.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
@@ -262,6 +272,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed', '--json']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
// @ts-ignore
|
||||
delete list.fake.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
@@ -284,6 +296,8 @@ describe('Driver CLI', function () {
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
// @ts-ignore
|
||||
delete list.fake.repositoryUrl;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getGitRev, getBuildInfo, updateBuildInfo, APPIUM_VER} from '../../lib/config';
|
||||
import axios from 'axios';
|
||||
import * as teenProcess from 'teen_process';
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe('Config', function () {
|
||||
@@ -14,6 +14,7 @@ describe('Config', function () {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.verify();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@@ -38,8 +39,10 @@ describe('Config', function () {
|
||||
|
||||
async function verifyBuildInfoUpdate(useLocalGit, {sha, built} = {}) {
|
||||
const buildInfo = getBuildInfo();
|
||||
|
||||
const innerExecStub = sandbox.stub().throws();
|
||||
if (!useLocalGit) {
|
||||
mockTp.expects('exec').atLeast(1).throws();
|
||||
sandbox.stub(teenProcess, 'exec').get(() => innerExecStub);
|
||||
}
|
||||
buildInfo['git-sha'] = undefined;
|
||||
buildInfo.built = undefined;
|
||||
@@ -56,17 +59,18 @@ describe('Config', function () {
|
||||
should.exist(buildInfo.built);
|
||||
}
|
||||
should.exist(buildInfo.version);
|
||||
|
||||
if (!useLocalGit) {
|
||||
expect(innerExecStub.callCount).to.be.at.least(1);
|
||||
}
|
||||
}
|
||||
|
||||
let getStub;
|
||||
let mockTp;
|
||||
beforeEach(function () {
|
||||
getStub = sandbox.stub(axios, 'get');
|
||||
mockTp = sandbox.mock(teenProcess);
|
||||
});
|
||||
afterEach(function () {
|
||||
getStub.restore();
|
||||
mockTp.restore();
|
||||
});
|
||||
|
||||
it('should get a configuration object if the local git metadata is present', async function () {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {fs, tempDir} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {runExtensionCommand} from '../../lib/cli/extension';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import {console, fs} from '@appium/support';
|
||||
import '@colors/colors';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {exec} from 'teen_process';
|
||||
import {APPIUM_ROOT, resolveFixture} from '../helpers';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
import {fs, tempDir} from '@appium/support';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {DRIVER_TYPE} from '../../lib/constants';
|
||||
import {resolveFixture} from '../helpers';
|
||||
import {installLocalExtension, runAppium} from './e2e-helpers';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import getPort from 'get-port';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import rewiremock, {addPlugin, overrideEntryPoint, plugins} from 'rewiremock';
|
||||
import {insertAppiumPrefixes} from '../lib/utils';
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {BaseDriver} from '@appium/base-driver';
|
||||
import {FakeDriver} from '@appium/fake-driver';
|
||||
import {sleep} from 'asyncbox';
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {finalizeSchema, registerSchema, resetSchema} from '../../lib/schema/schema';
|
||||
import {insertAppiumPrefixes, removeAppiumPrefixes} from '../../lib/utils';
|
||||
|
||||
@@ -2,7 +2,6 @@ import {tempDir, fs, npm} from '@appium/support';
|
||||
import {loadExtensions} from '../../../lib/extension';
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import DriverCommand from '../../../lib/cli/driver-command';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('DriverCommand', function () {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {finalizeSchema, resetSchema, SchemaFinalizationError} from '../../../lib/schema/schema';
|
||||
import {rewiremock} from '../../helpers';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {system} from '@appium/support';
|
||||
import {getPresetDrivers} from '../../../lib/cli/setup-command';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'fs';
|
||||
// eslint-disable-next-line import/named
|
||||
import fs from 'node:fs';
|
||||
import {createSandbox} from 'sinon';
|
||||
import * as YAML from 'yaml';
|
||||
import * as schema from '../../lib/schema/schema';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getParser} from '../../lib/cli/parser';
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import {promises as fs} from 'fs';
|
||||
import {promises as fs} from 'node:fs';
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import {resetSchema} from '../../../lib/schema';
|
||||
import {resolveFixture, rewiremock} from '../../helpers';
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
// @ts-check
|
||||
import {DriverConfig} from '../../../lib/extension/driver-config';
|
||||
import {ExtensionCommand} from '../../../lib/cli/extension-command';
|
||||
import {ExtensionCommand, injectAppiumSymlinks} from '../../../lib/cli/extension-command';
|
||||
import sinon from 'sinon';
|
||||
import sinonChai from 'sinon-chai';
|
||||
import {FAKE_DRIVER_DIR} from '../../helpers';
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import {fs, system} from '@appium/support';
|
||||
import * as utils from '../../../lib/utils';
|
||||
import {expect} from 'chai';
|
||||
import * as chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
/**
|
||||
* Relative path from actual `package.json` of `FakeDriver` for the `fake-stdin` script
|
||||
*/
|
||||
const FAKE_STDIN_SCRIPT = require(`${FAKE_DRIVER_DIR}/package.json`).appium.scripts['fake-stdin'];
|
||||
|
||||
/** @type {sinon.SinonSandbox} */
|
||||
let sandbox;
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(sinonChai);
|
||||
|
||||
describe('ExtensionCommand', function () {
|
||||
describe('method', function () {
|
||||
/** @type {ExtensionCommand} */
|
||||
/** @type {ExtensionCommand<any>} */
|
||||
let ec;
|
||||
|
||||
/** @type {sinon.SinonSandbox} */
|
||||
let sandbox;
|
||||
|
||||
let expect;
|
||||
|
||||
before(async function () {
|
||||
const chai = await import('chai');
|
||||
const chaiAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaiAsPromised.default);
|
||||
chai.should();
|
||||
expect = chai.expect;
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
const driverConfig = DriverConfig.create(sandbox.createStubInstance(Manifest));
|
||||
@@ -35,6 +34,7 @@ describe('ExtensionCommand', function () {
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.verify();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@@ -46,18 +46,19 @@ describe('ExtensionCommand', function () {
|
||||
// `Promise` + `ChildProcess`, but I didn't want to add the dep.
|
||||
it('should respond to stdin', function (done) {
|
||||
// we have to fake writing to STDIN because this is an automated test, after all.
|
||||
const proc = ec
|
||||
const proc = /** @type {import('child_process').ChildProcess} */ (ec
|
||||
._runUnbuffered(FAKE_DRIVER_DIR, FAKE_STDIN_SCRIPT, [], {
|
||||
stdio: ['pipe', 'inherit', 'inherit'],
|
||||
})
|
||||
.once('exit', (code) => {
|
||||
try {
|
||||
expect(code).to.equal(0);
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
proc.once('exit', (/** @type {number | null} */ code) => {
|
||||
try {
|
||||
expect(code).to.equal(0);
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// TS does not understand that `proc.stdin` is not `null`, because it is only a `Writable`
|
||||
@@ -69,4 +70,349 @@ describe('ExtensionCommand', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectAppiumSymlinks', function () {
|
||||
|
||||
/** @type {sinon.SinonStub} */
|
||||
let fsExistsStub;
|
||||
|
||||
/** @type {sinon.SinonStub} */
|
||||
let fsSymlinkStub;
|
||||
|
||||
/** @type {sinon.SinonStub} */
|
||||
let getAppiumModuleRootStub;
|
||||
|
||||
/** @type {sinon.SinonStub} */
|
||||
let isWindowsStub;
|
||||
|
||||
/** @type {import('@appium/types').AppiumLogger} */
|
||||
let logger;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
fsExistsStub = sandbox.stub(fs, 'exists');
|
||||
fsSymlinkStub = sandbox.stub(fs, 'symlink');
|
||||
getAppiumModuleRootStub = sandbox.stub(utils, 'getAppiumModuleRoot');
|
||||
isWindowsStub = sandbox.stub(system, 'isWindows');
|
||||
logger = /** @type {any} */ ({
|
||||
info: sandbox.stub(),
|
||||
warn: sandbox.stub(),
|
||||
error: sandbox.stub(),
|
||||
debug: sandbox.stub(),
|
||||
});
|
||||
|
||||
getAppiumModuleRootStub.returns('/path/to/appium');
|
||||
isWindowsStub.returns(false);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.verify();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('when there are no installed extensions', function () {
|
||||
it('should not create any symlinks', async function () {
|
||||
const driverConfig = {installedExtensions: {}};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are npm-installed drivers', function () {
|
||||
it('should create symlinks for npm-installed drivers', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules').resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsExistsStub).to.have.been.calledWith('/path/to/driver-for-test/node_modules');
|
||||
expect(fsExistsStub).to.have.been.calledWith('/path/to/driver-for-test/node_modules/appium');
|
||||
expect(fsSymlinkStub).to.have.been.calledOnce;
|
||||
expect(fsSymlinkStub).to.have.been.calledWith(
|
||||
'/path/to/appium',
|
||||
'/path/to/driver-for-test/node_modules/appium',
|
||||
'dir'
|
||||
);
|
||||
});
|
||||
|
||||
it('should create junction symlinks on Windows', async function () {
|
||||
isWindowsStub.returns(true);
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules').resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.have.been.calledWith(
|
||||
'/path/to/appium',
|
||||
'/path/to/driver-for-test/node_modules/appium',
|
||||
'junction'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create symlinks if node_modules directory does not exist', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should not create symlinks if symlink already exists', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.resolves(true);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are npm-installed plugins', function () {
|
||||
it('should create symlinks for npm-installed plugins', async function () {
|
||||
const driverConfig = {installedExtensions: {}};
|
||||
const pluginConfig = {
|
||||
installedExtensions: {
|
||||
'plugin-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/plugin-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fsExistsStub.withArgs('/path/to/plugin-for-test/node_modules').resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/plugin-for-test/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.have.been.calledOnce;
|
||||
expect(fsSymlinkStub).to.have.been.calledWith(
|
||||
'/path/to/appium',
|
||||
'/path/to/plugin-for-test/node_modules/appium',
|
||||
'dir'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are both drivers and plugins', function () {
|
||||
it('should create symlinks for all npm-installed extensions', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {
|
||||
installedExtensions: {
|
||||
'plugin-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/plugin-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fsExistsStub.resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules/appium').resolves(false);
|
||||
fsExistsStub.withArgs('/path/to/plugin-for-test/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('should not create symlinks for invalid format - no installPath', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {
|
||||
installedExtensions: {
|
||||
'plugin-for-test': {
|
||||
installType: 'npm',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are non-npm installed extensions', function () {
|
||||
for (const installType of ['git', 'local', 'github']) {
|
||||
it(`should skip ${installType}-installed extensions`, async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
[`${installType}-driver`]: {
|
||||
installType,
|
||||
installPath: `/path/to/${installType}-driver`,
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
}
|
||||
|
||||
it('should only create symlinks for npm-installed extensions when mixed', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'npm-driver': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/npm-driver',
|
||||
},
|
||||
'git-driver': {
|
||||
installType: 'git',
|
||||
installPath: '/path/to/git-driver',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/npm-driver/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.have.been.calledOnce;
|
||||
expect(fsSymlinkStub).to.have.been.calledWith(
|
||||
'/path/to/appium',
|
||||
'/path/to/npm-driver/node_modules/appium',
|
||||
'dir'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', function () {
|
||||
it('should log info message when symlink creation fails', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
fsExistsStub.resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules/appium').resolves(false);
|
||||
fsSymlinkStub.rejects(new Error('Permission denied'));
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(logger.info).to.have.been.calledOnce;
|
||||
// @ts-ignore
|
||||
expect(logger.info.args[0][0]).to.match(/Cannot create a symlink/);
|
||||
// @ts-ignore
|
||||
expect(logger.info.args[0][0]).to.match(/Permission denied/);
|
||||
});
|
||||
|
||||
it('should not throw when getAppiumModuleRoot fails', async function () {
|
||||
const driverConfig = {
|
||||
installedExtensions: {
|
||||
'driver-for-test': {
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/driver-for-test',
|
||||
},
|
||||
},
|
||||
};
|
||||
const pluginConfig = {installedExtensions: {}};
|
||||
|
||||
getAppiumModuleRootStub.throws(new Error('Module not found'));
|
||||
fsExistsStub.resolves(true);
|
||||
fsExistsStub.withArgs('/path/to/driver-for-test/node_modules/appium').resolves(false);
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(logger.info).to.have.been.calledOnce;
|
||||
// @ts-ignore
|
||||
expect(logger.info.args[0][0]).to.match(/Cannot create a symlink/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with null or undefined configs', function () {
|
||||
it('should handle null installedExtensions', async function () {
|
||||
const driverConfig = {installedExtensions: null};
|
||||
const pluginConfig = {installedExtensions: null};
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should handle undefined installedExtensions', async function () {
|
||||
const driverConfig = {};
|
||||
const pluginConfig = {};
|
||||
|
||||
// @ts-expect-error - partial config for testing
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
|
||||
expect(fsSymlinkStub).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {DRIVER_TYPE} from '../../../lib/constants';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import {FAKE_DRIVER_DIR, PROJECT_ROOT, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @ts-check
|
||||
import B from 'bluebird';
|
||||
import {promises as fs} from 'fs';
|
||||
import {promises as fs} from 'node:fs';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {resolveFixture, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import EventEmitter from 'events';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
describe('Manifest', function () {
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* A collection of mocks reused across unit tests.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import path from 'path';
|
||||
// eslint-disable-next-line import/named
|
||||
import {EventEmitter} from 'node:events';
|
||||
import path from 'node:path';
|
||||
import {createSandbox} from 'sinon';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {PKG_HASHFILE_RELATIVE_PATH} from '../../../lib/constants';
|
||||
import {rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import {promises as fs} from 'fs';
|
||||
import {promises as fs} from 'node:fs';
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import {resetSchema} from '../../../lib/schema';
|
||||
import {resolveFixture, rewiremock} from '../../helpers';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {rewiremock} from '../helpers';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {init as logsinkInit, clear as logsinkClear} from '../../lib/logsink';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {logger} from '@appium/support';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {AppiumConfigJsonSchema} from '@appium/schema';
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import {BASE_CAPS, W3C_CAPS} from '../helpers';
|
||||
import _ from 'lodash';
|
||||
import {stripColors} from '@colors/colors';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import logger from '../../lib/logger';
|
||||
import {fs} from '@appium/support';
|
||||
|
||||
@@ -26,7 +26,7 @@ export type CliCommandSetupSubcommand = 'mobile' | 'browser' | 'desktop' | 'rese
|
||||
* Possible subcommands of {@linkcode CliCommandDriver} or
|
||||
* {@linkcode CliCommandPlugin}.
|
||||
*/
|
||||
export type CliExtensionSubcommand = 'install' | 'list' | 'run' | 'uninstall' | 'update'| 'doctor';
|
||||
export type CliExtensionSubcommand = 'install' | 'list' | 'run' | 'uninstall' | 'update' | 'doctor';
|
||||
|
||||
export interface CliExtensionSubcommandListArgs {
|
||||
showInstalled?: boolean;
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [10.2.0](https://github.com/appium/appium/compare/@appium/base-driver@10.1.2...@appium/base-driver@10.2.0) (2026-01-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **base-driver:** add ability to supply custom headers for proxy ([#21790](https://github.com/appium/appium/issues/21790)) ([a4b47c0](https://github.com/appium/appium/commit/a4b47c02c6e72381fa9d191d2a0bced106999705))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* apply npm run lint:fix ([#21867](https://github.com/appium/appium/issues/21867)) ([5e28714](https://github.com/appium/appium/commit/5e28714442cf59ee35b085e01e82c3f5483891fd))
|
||||
|
||||
|
||||
|
||||
## [10.1.2](https://github.com/appium/appium/compare/@appium/base-driver@10.1.1...@appium/base-driver@10.1.2) (2025-12-04)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-driver
|
||||
|
||||
@@ -28,7 +28,7 @@ export type ParsedCaps<C extends Constraints> = {
|
||||
export type ValidateCapsOpts = {
|
||||
/** if true, skip the presence constraint */
|
||||
skipPresenceConstraint?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes primary caps object and merges it into a secondary caps object.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import path from 'node:path';
|
||||
import logger from './logger';
|
||||
import {tempDir, fs, util, timing, node} from '@appium/support';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
@@ -354,12 +353,16 @@ export function generateDriverLogPrefix(obj, sessionId = null) {
|
||||
* @returns {Promise<RemoteAppData>}
|
||||
*/
|
||||
async function queryAppLink(appLink, reqHeaders) {
|
||||
const {href, auth} = url.parse(appLink);
|
||||
const axiosUrl = auth ? href.replace(`${auth}@`, '') : href;
|
||||
const url = new URL(appLink);
|
||||
// Extract credentials, then remove them from the URL for axios
|
||||
const {username, password} = url;
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
const axiosUrl = url.href;
|
||||
/** @type {import('axios').AxiosBasicCredentials|undefined} */
|
||||
const axiosAuth = auth ? {
|
||||
username: auth.substring(0, auth.indexOf(':')),
|
||||
password: auth.substring(auth.indexOf(':') + 1),
|
||||
const axiosAuth = username ? {
|
||||
username,
|
||||
password,
|
||||
} : undefined;
|
||||
/**
|
||||
* @type {import('axios').RawAxiosRequestConfig}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import log from './logger';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import _ from 'lodash';
|
||||
import {EventEmitter} from 'events';
|
||||
import {EventEmitter} from 'node:events';
|
||||
|
||||
const IDEMPOTENT_RESPONSES = new LRUCache({
|
||||
max: 64,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import http from 'node:http';
|
||||
import favicon from 'serve-favicon';
|
||||
import bodyParser from 'body-parser';
|
||||
import methodOverride from 'method-override';
|
||||
@@ -122,7 +122,6 @@ export function configureServer({
|
||||
|
||||
// set up static assets
|
||||
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
app.use(express.static(STATIC_DIR));
|
||||
|
||||
// crash routes, for testing
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import log from './logger';
|
||||
import _ from 'lodash';
|
||||
import {fs} from '@appium/support';
|
||||
|
||||
@@ -12,8 +12,8 @@ import {isSessionCommand, routeToCommandName} from '../protocol';
|
||||
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
||||
import ProtocolConverter from './protocol-converter';
|
||||
import {formatResponseValue, ensureW3cResponse} from '../protocol/helpers';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { match as pathToRegexMatch } from 'path-to-regexp';
|
||||
import nodeUrl from 'node:url';
|
||||
import { ProxyRequest } from './proxy-request';
|
||||
@@ -34,6 +34,7 @@ const ALLOWED_OPTS = [
|
||||
'timeout',
|
||||
'log',
|
||||
'keepAlive',
|
||||
'headers',
|
||||
];
|
||||
|
||||
export class JWProxy {
|
||||
@@ -51,6 +52,8 @@ export class JWProxy {
|
||||
sessionId;
|
||||
/** @type {number} */
|
||||
timeout;
|
||||
/** @type {import('@appium/types').HTTPHeaders | undefined} */
|
||||
headers;
|
||||
/** @type {Protocol | null | undefined} */
|
||||
_downstreamProtocol;
|
||||
/** @type {ProxyRequest[]} */
|
||||
@@ -192,6 +195,7 @@ export class JWProxy {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'user-agent': 'appium',
|
||||
accept: 'application/json, */*',
|
||||
...(this.headers ?? {}),
|
||||
},
|
||||
proxy: false,
|
||||
timeout: this.timeout,
|
||||
@@ -439,6 +443,7 @@ export class JWProxy {
|
||||
* @returns {ParsedUrl}
|
||||
*/
|
||||
_parseUrl(url) {
|
||||
// eslint-disable-next-line n/no-deprecated-api -- we need relative URL support
|
||||
const parsedUrl = nodeUrl.parse(url || '/');
|
||||
if (
|
||||
_.isNil(parsedUrl.href) || _.isNil(parsedUrl.pathname)
|
||||
|
||||
@@ -74,7 +74,7 @@ export class ProtocolError extends BaseError {
|
||||
* @see https://w3c.github.io/webdriver-bidi/#protocol-definition
|
||||
* @returns The object conforming to the shape of a BiDi error response
|
||||
*/
|
||||
bidiErrObject(id: string|number): ErrorBiDiCommandResponse {
|
||||
bidiErrObject(id: string | number): ErrorBiDiCommandResponse {
|
||||
// if we don't have an id, the client didn't send one, so we have nothing to send back.
|
||||
// send back zero rather than making something up
|
||||
const intId = (_.isInteger(id) ? id : (parseInt(`${id}`, 10) || 0)) as number;
|
||||
@@ -1116,6 +1116,6 @@ interface W3CError {
|
||||
}
|
||||
|
||||
interface ParameterRequirements {
|
||||
required: string[]|string;
|
||||
optional?: string[]|string;
|
||||
required: string[] | string;
|
||||
optional?: string[] | string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "10.1.2",
|
||||
"version": "10.2.0",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -34,7 +34,8 @@
|
||||
"static",
|
||||
"build",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "run-p test:unit test:types",
|
||||
@@ -44,25 +45,24 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^7.0.4",
|
||||
"@appium/types": "^1.1.2",
|
||||
"@appium/support": "^7.0.5",
|
||||
"@appium/types": "^1.2.0",
|
||||
"@colors/colors": "1.6.0",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.13.2",
|
||||
"asyncbox": "6.0.1",
|
||||
"axios": "1.13.3",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "2.2.1",
|
||||
"body-parser": "2.2.2",
|
||||
"express": "5.2.1",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"http-status-codes": "2.3.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.2.4",
|
||||
"lodash": "4.17.23",
|
||||
"lru-cache": "11.2.5",
|
||||
"method-override": "3.0.0",
|
||||
"morgan": "1.10.1",
|
||||
"path-to-regexp": "8.3.0",
|
||||
"serve-favicon": "2.5.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "5.3.0"
|
||||
"type-fest": "5.4.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"spdy": "4.0.2"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import path from 'node:path';
|
||||
import {fs} from '@appium/support';
|
||||
import {configureApp} from '../../../lib/basedriver/helpers';
|
||||
import http from 'http';
|
||||
import http from 'node:http';
|
||||
import finalhandler from 'finalhandler';
|
||||
import serveStatic from 'serve-static';
|
||||
import contentDisposition from 'content-disposition';
|
||||
@@ -83,7 +82,7 @@ describe('app download and configuration', function () {
|
||||
return;
|
||||
}
|
||||
// for testing zip file content types
|
||||
const contentType = new URLSearchParams(url.parse(req.url).search).get('content-type');
|
||||
const contentType = new URLSearchParams(new URL(req.url, 'http://localhost').search).get('content-type');
|
||||
if (contentType !== null) {
|
||||
res.setHeader('content-type', contentType);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {server, routeConfiguringFunction} from '../../../lib';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getTestPort, TEST_HOST} from '@appium/driver-test-support';
|
||||
import {MockExecuteDriver} from '../protocol/mock-execute-driver';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {server} from '../../../lib';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {server, routeConfiguringFunction, errors, JWProxy, BaseDriver} from '../../../lib';
|
||||
import {FakeDriver} from './fake-driver';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {StatusCodes as HTTPStatusCodes} from 'http-status-codes';
|
||||
import {createProxyServer} from './helpers';
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import B from 'bluebird';
|
||||
import {BaseDriver, errors} from '../../../lib/index';
|
||||
import {validator} from '../../../lib/basedriver/validation';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
// TODO: we need module-level mocks for the logger
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-check
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import _ from 'lodash';
|
||||
import {BaseDriver} from '../../../../lib';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import {BaseDriver} from '../../../lib';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('timeout', function () {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {server, routeConfiguringFunction} from '../../../lib';
|
||||
import {configureServer, normalizeBasePath} from '../../../lib/express/server';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getTestPort} from '@appium/driver-test-support';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {welcome} from '../../../lib/express/static';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('welcome', function () {
|
||||
|
||||
@@ -149,6 +149,30 @@ describe('proxy', function () {
|
||||
res.statusCode.should.equal(200);
|
||||
body.should.eql({status: 0, value: {foo: 'bar'}});
|
||||
});
|
||||
it('should apply custom headers to downstream requests', async function () {
|
||||
const customHeaders = {
|
||||
'x-custom-header': 'foobar',
|
||||
'user-agent': 'my-appium-client',
|
||||
};
|
||||
let capturedConfig;
|
||||
const j = mockProxy({headers: customHeaders});
|
||||
j.request = async function (config) {
|
||||
capturedConfig = config;
|
||||
return await request(config);
|
||||
};
|
||||
await j.proxy('/status', 'GET');
|
||||
capturedConfig.should.have.property('headers');
|
||||
capturedConfig.headers.should.have.property('x-custom-header', 'foobar');
|
||||
capturedConfig.headers.should.have.property('user-agent', 'my-appium-client');
|
||||
capturedConfig.headers.should.have.property(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
capturedConfig.headers.should.have.property(
|
||||
'accept',
|
||||
'application/json, */*'
|
||||
);
|
||||
});
|
||||
it('should pass along request errors', function () {
|
||||
const j = mockProxy({sessionId: '123'});
|
||||
j.proxy('/badurl', 'GET').should.eventually.be.rejectedWith('Could not proxy');
|
||||
|
||||
@@ -2,7 +2,7 @@ import {errors, errorFromMJSONWPStatusCode, errorFromW3CJsonCode, isErrorType} f
|
||||
import {BadParametersError, getResponseForW3CError} from '../../../lib/protocol/errors';
|
||||
import _ from 'lodash';
|
||||
import {StatusCodes as HTTPStatusCodes} from 'http-status-codes';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
|
||||
const basename = path.basename(__filename);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import {METHOD_MAP, routeToCommandName} from '../../../lib/protocol';
|
||||
import crypto from 'crypto';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
describe('Routes', function () {
|
||||
// TODO test against an explicit protocol rather than a hash of a previous
|
||||
|
||||
@@ -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.6](https://github.com/appium/appium/compare/@appium/base-plugin@3.0.5...@appium/base-plugin@3.0.6) (2026-01-26)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.5](https://github.com/appium/appium/compare/@appium/base-plugin@3.0.4...@appium/base-plugin@3.0.5) (2025-12-04)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.6",
|
||||
"description": "The base plugin used to create Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -32,7 +32,8 @@
|
||||
"build",
|
||||
"index.js",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
@@ -40,8 +41,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.1.2",
|
||||
"@appium/support": "^7.0.4"
|
||||
"@appium/base-driver": "^10.2.0",
|
||||
"@appium/support": "^7.0.5"
|
||||
},
|
||||
"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.
|
||||
|
||||
## [2.2.1](https://github.com/appium/appium/compare/@appium/docutils@2.2.0...@appium/docutils@2.2.1) (2026-01-26)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.0](https://github.com/appium/appium/compare/@appium/docutils@2.1.2...@appium/docutils@2.2.0) (2025-12-04)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// @ts-check
|
||||
|
||||
'use strict';
|
||||
require('source-map-support').install();
|
||||
|
||||
const {main} = require('../build/lib/cli');
|
||||
const {getLogger} = require('../build/lib/logger');
|
||||
|
||||
@@ -10,8 +10,7 @@ import {getLogger} from '../logger';
|
||||
|
||||
import {fs} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {sync as readPkg} from 'read-pkg';
|
||||
import {readPackageSync} from 'read-pkg';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import yargs from 'yargs/yargs';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap, NAME_BIN} from '../constants';
|
||||
@@ -19,7 +18,7 @@ import {DocutilsError} from '../error';
|
||||
import {build, init, validate} from './command';
|
||||
import {findConfig} from './config';
|
||||
|
||||
const pkg = readPkg({cwd: fs.findRoot(__dirname)});
|
||||
const pkg = readPackageSync({cwd: fs.findRoot(__dirname)});
|
||||
const log = getLogger('cli');
|
||||
const IMPLICATIONS_FAILED_REGEX = /implications\s+failed:\n\s*(.+)\s->\s(.+)$/i;
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import {fs} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import path from 'node:path';
|
||||
import _pkgDir from 'pkg-dir';
|
||||
import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg';
|
||||
import {packageDirectory} from 'package-directory';
|
||||
import {readPackage, NormalizedPackageJson, PackageJson} from 'read-pkg';
|
||||
import {JsonValue} from 'type-fest';
|
||||
import * as YAML from 'yaml';
|
||||
import {
|
||||
@@ -30,7 +30,7 @@ const log = getLogger('fs');
|
||||
*
|
||||
* Caches result
|
||||
*/
|
||||
const findPkgDir = _.memoize(_pkgDir);
|
||||
const findPkgDir = _.memoize(packageDirectory, (opts) => opts?.cwd);
|
||||
|
||||
/**
|
||||
* Stringifies a thing into a YAML
|
||||
@@ -75,7 +75,7 @@ export async function findInPkgDir(
|
||||
filename: string,
|
||||
cwd = process.cwd(),
|
||||
): Promise<string | undefined> {
|
||||
const pkgDir = await findPkgDir(cwd);
|
||||
const pkgDir = await findPkgDir({cwd});
|
||||
if (!pkgDir) {
|
||||
return;
|
||||
}
|
||||
@@ -109,7 +109,7 @@ async function _readPkgJson(
|
||||
cwd: string,
|
||||
normalize?: boolean,
|
||||
): Promise<{pkgPath: string; pkg: PackageJson | NormalizedPackageJson}> {
|
||||
const pkgDir = await findPkgDir(cwd);
|
||||
const pkgDir = await findPkgDir({cwd});
|
||||
if (!pkgDir) {
|
||||
throw new DocutilsError(
|
||||
`Could not find a ${NAME_PACKAGE_JSON} near ${cwd}; please create it before using this utility`,
|
||||
@@ -118,10 +118,10 @@ async function _readPkgJson(
|
||||
const pkgPath = path.join(pkgDir, NAME_PACKAGE_JSON);
|
||||
log.debug('Found `package.json` at %s', pkgPath);
|
||||
if (normalize) {
|
||||
const pkg = await readPkg({cwd: pkgDir, normalize});
|
||||
const pkg = await readPackage({cwd: pkgDir, normalize});
|
||||
return {pkg, pkgPath};
|
||||
} else {
|
||||
const pkg = await readPkg({cwd: pkgDir});
|
||||
const pkg = await readPackage({cwd: pkgDir});
|
||||
return {pkg, pkgPath};
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export function writeFileString(filepath: string, content: JsonValue) {
|
||||
});
|
||||
}
|
||||
|
||||
type WhichFunction = (cmd: string, opts?: {nothrow: boolean}) => Promise<string|null>;
|
||||
type WhichFunction = (cmd: string, opts?: {nothrow: boolean}) => Promise<string | null>;
|
||||
|
||||
/**
|
||||
* `which` with memoization
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import {fs, util} from '@appium/support';
|
||||
import chalk from 'chalk';
|
||||
import _ from 'lodash';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {exec} from 'teen_process';
|
||||
@@ -21,12 +20,7 @@ import {
|
||||
REQUIREMENTS_TXT_PATH,
|
||||
} from './constants';
|
||||
import {DocutilsError} from './error';
|
||||
import {
|
||||
findMkDocsYml,
|
||||
isMkDocsInstalled,
|
||||
readMkDocsYml,
|
||||
findPython,
|
||||
} from './fs';
|
||||
import {findMkDocsYml, isMkDocsInstalled, readMkDocsYml, findPython} from './fs';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, PipPackage} from './model';
|
||||
|
||||
@@ -319,7 +313,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
]));
|
||||
} catch {
|
||||
return this.fail(
|
||||
`Could not find ${NAME_PIP} installation for Python at ${pythonPath}. Is it installed?`
|
||||
`Could not find ${NAME_PIP} installation for Python at ${pythonPath}. Is it installed?`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,7 +346,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
'package',
|
||||
missingPackages.length,
|
||||
)} could not be found:\n${missingPackages
|
||||
.map((p) => chalk`- {yellow ${p.name}} @ {yellow ${p.version}}`)
|
||||
.map((p) => `- ${p.name} @ ${p.version}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
@@ -364,7 +358,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
)} are installed, but at the wrong version:\n${invalidVersionPackages
|
||||
.map(
|
||||
([expected, actual]) =>
|
||||
chalk`- {yellow ${expected.name}} @ {yellow ${expected.version}} (found {red ${actual.version}})`,
|
||||
`- ${expected.name} @ ${expected.version} (found ${actual.version})`,
|
||||
)
|
||||
.join('\n')}`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -38,6 +38,7 @@
|
||||
"build",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test",
|
||||
"requirements.txt"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -48,17 +49,15 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^7.0.4",
|
||||
"chalk": "4.1.2",
|
||||
"@appium/support": "^7.0.5",
|
||||
"consola": "3.4.2",
|
||||
"diff": "8.0.2",
|
||||
"diff": "8.0.3",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
"pkg-dir": "5.0.0",
|
||||
"read-pkg": "5.2.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "3.0.4",
|
||||
"type-fest": "5.3.0",
|
||||
"lodash": "4.17.23",
|
||||
"package-directory": "8.1.0",
|
||||
"read-pkg": "10.0.0",
|
||||
"teen_process": "4.0.8",
|
||||
"type-fest": "5.4.1",
|
||||
"yaml": "2.8.2",
|
||||
"yargs": "18.0.0",
|
||||
"yargs-parser": "22.0.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-git-revision-date-localized-plugin==1.5.0
|
||||
mkdocs-material==9.7.0
|
||||
mkdocs-material==9.7.1
|
||||
mkdocs-redirects==1.2.2
|
||||
mike==2.1.3
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.6](https://github.com/appium/appium/compare/@appium/driver-test-support@1.0.5...@appium/driver-test-support@1.0.6) (2026-01-26)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.5](https://github.com/appium/appium/compare/@appium/driver-test-support@1.0.4...@appium/driver-test-support@1.0.5) (2025-12-04)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
// wrap these tests in a function so we can export the tests and re-use them
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -32,7 +32,8 @@
|
||||
"lib",
|
||||
"build",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
@@ -41,16 +42,15 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^1.1.2",
|
||||
"axios": "1.13.2",
|
||||
"@appium/types": "^1.2.0",
|
||||
"axios": "1.13.3",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "6.2.1",
|
||||
"chai": "6.2.2",
|
||||
"chai-as-promised": "8.0.2",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "21.0.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "5.3.0"
|
||||
"get-port": "7.1.0",
|
||||
"lodash": "4.17.23",
|
||||
"sinon": "21.0.1",
|
||||
"type-fest": "5.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^3.0.0-beta.0",
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@2.0.5...@appium/eslint-config-appium-ts@3.0.0) (2026-01-26)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **eslint-config-appium-ts:** modernize ESLint config (#21781)
|
||||
|
||||
### Features
|
||||
|
||||
* **eslint-config-appium-ts:** add unicorn/prefer-node-protocol with warn ([#21864](https://github.com/appium/appium/issues/21864)) ([77c9b58](https://github.com/appium/appium/commit/77c9b5898907d7380276d5982c7e175d10440c73))
|
||||
* **eslint-config-appium-ts:** modernize ESLint config ([#21781](https://github.com/appium/appium/issues/21781)) ([b0b219a](https://github.com/appium/appium/commit/b0b219aa019d893bf30e90203a8265625777c47f))
|
||||
|
||||
|
||||
|
||||
## [2.0.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@2.0.4...@appium/eslint-config-appium-ts@2.0.5) (2025-12-04)
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,21 @@ Install the package:
|
||||
npm install @appium/eslint-config-appium-ts --save-dev
|
||||
```
|
||||
|
||||
And then, in your `eslint.config.mjs` file, extend the configuration:
|
||||
Then add it to your `eslint.config.mjs` file:
|
||||
|
||||
```js
|
||||
import appiumConfig from '@appium/eslint-config-appium-ts';
|
||||
import {defineConfig} from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
extends: [appiumConfig],
|
||||
// add any other config changes
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Or for ESLint `< 9.22.0`:
|
||||
|
||||
```js
|
||||
import appiumConfig from '@appium/eslint-config-appium-ts';
|
||||
|
||||
@@ -3,29 +3,27 @@ import fs from 'node:fs';
|
||||
|
||||
import {includeIgnoreFile} from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import {createTypeScriptImportResolver} from 'eslint-import-resolver-typescript';
|
||||
import globals from 'globals';
|
||||
import pluginPromise from 'eslint-plugin-promise';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import {importX} from 'eslint-plugin-import-x';
|
||||
import mochaPlugin from 'eslint-plugin-mocha';
|
||||
import {configs as tsConfigs, parser as tsParser, plugin as tsPlugin} from 'typescript-eslint';
|
||||
import nodePlugin from 'eslint-plugin-n';
|
||||
import {configs as tsConfigs} from 'typescript-eslint';
|
||||
import unicorn from 'eslint-plugin-unicorn';
|
||||
|
||||
const gitignorePath = path.resolve(process.cwd(), '.gitignore');
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
pluginPromise.configs['flat/recommended'],
|
||||
importPlugin.flatConfigs.recommended,
|
||||
...tsConfigs.recommended,
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'Script Files',
|
||||
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
parser: tsParser,
|
||||
globals: {
|
||||
...globals.node,
|
||||
NodeJS: 'readonly',
|
||||
@@ -33,22 +31,51 @@ export default [
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
'@stylistic': stylistic,
|
||||
'import-x': importX,
|
||||
js,
|
||||
n: nodePlugin,
|
||||
promise: pluginPromise,
|
||||
unicorn
|
||||
},
|
||||
extends: [
|
||||
'js/recommended',
|
||||
'promise/flat/recommended',
|
||||
'import-x/flat/recommended',
|
||||
tsConfigs.recommended,
|
||||
eslintConfigPrettier,
|
||||
],
|
||||
settings: {
|
||||
/**
|
||||
* This stuff enables `eslint-plugin-import` to resolve TS modules.
|
||||
*/
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx', '.mtsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
project: ['tsconfig.json', './packages/*/tsconfig.json'],
|
||||
},
|
||||
},
|
||||
})
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
'@stylistic/array-bracket-spacing': 'error',
|
||||
'@stylistic/arrow-parens': 'warn',
|
||||
'@stylistic/arrow-spacing': 'error',
|
||||
'@stylistic/comma-spacing': 'error',
|
||||
'@stylistic/key-spacing': 'error',
|
||||
'@stylistic/keyword-spacing': 'error',
|
||||
'@stylistic/no-multi-spaces': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
'@stylistic/no-whitespace-before-property': 'error',
|
||||
'@stylistic/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: 'always',
|
||||
},
|
||||
],
|
||||
'@stylistic/semi': 'error',
|
||||
'@stylistic/space-before-blocks': 'error',
|
||||
'@stylistic/space-in-parens': 'error',
|
||||
'@stylistic/space-infix-ops': 'error',
|
||||
'@stylistic/space-unary-ops': 'error',
|
||||
|
||||
/**
|
||||
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
|
||||
* without explanation.
|
||||
@@ -71,7 +98,6 @@ export default [
|
||||
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
|
||||
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
|
||||
*/
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
/**
|
||||
* Explicit `any` types are allowed.
|
||||
@@ -93,76 +119,28 @@ export default [
|
||||
* Sometimes we want unused variables to be present in base class method declarations.
|
||||
*/
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
|
||||
'import/export': 2,
|
||||
'import/named': 'warn',
|
||||
'import/no-duplicates': 2,
|
||||
'import/no-unresolved': 2,
|
||||
'import-x/named': 'warn',
|
||||
'import-x/no-duplicates': 'error',
|
||||
|
||||
'promise/catch-or-return': 1,
|
||||
/**
|
||||
* Allow native `Promise`s.
|
||||
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
|
||||
* everywhere, but this is not strictly necessary.
|
||||
*/
|
||||
'promise/no-native': 'off',
|
||||
'promise/no-return-wrap': 1,
|
||||
'promise/prefer-await-to-callbacks': 1,
|
||||
'promise/prefer-await-to-then': 1,
|
||||
'promise/param-names': 1,
|
||||
'n/no-deprecated-api': 'warn',
|
||||
|
||||
'array-bracket-spacing': 2,
|
||||
'arrow-body-style': [1, 'as-needed'],
|
||||
'arrow-parens': [1, 'always'],
|
||||
'arrow-spacing': 2,
|
||||
/**
|
||||
* Disables the `brace-style` rule.
|
||||
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
|
||||
* to indent in a way that is not allowed by the default `brace-style` rule.
|
||||
*/
|
||||
'brace-style': 'off',
|
||||
'comma-dangle': 0,
|
||||
'comma-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
'curly': [2, 'all'],
|
||||
'dot-notation': 2,
|
||||
'eqeqeq': [2, 'smart'],
|
||||
'key-spacing': [
|
||||
2,
|
||||
{
|
||||
mode: 'strict',
|
||||
beforeColon: false,
|
||||
afterColon: true,
|
||||
},
|
||||
],
|
||||
'keyword-spacing': 2,
|
||||
'no-buffer-constructor': 1,
|
||||
'no-console': 2,
|
||||
'no-dupe-class-members': 'off',
|
||||
'no-empty': 0,
|
||||
'no-multi-spaces': 2,
|
||||
'no-prototype-builtins': 1,
|
||||
'no-redeclare': 'off',
|
||||
'no-trailing-spaces': 2,
|
||||
'no-var': 2,
|
||||
'no-whitespace-before-property': 2,
|
||||
'object-shorthand': 2,
|
||||
'quotes': [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'radix': [2, 'always'],
|
||||
'require-atomic-updates': 0,
|
||||
'promise/catch-or-return': 'warn',
|
||||
'promise/no-return-wrap': 'warn',
|
||||
'promise/prefer-await-to-callbacks': 'warn',
|
||||
'promise/prefer-await-to-then': 'warn',
|
||||
'promise/param-names': 'warn',
|
||||
|
||||
'arrow-body-style': 'warn',
|
||||
'curly': 'error',
|
||||
'dot-notation': 'error',
|
||||
'eqeqeq': ['error', 'smart'],
|
||||
'no-console': 'error',
|
||||
'no-empty': 'off',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'object-shorthand': 'error',
|
||||
'radix': 'error',
|
||||
'require-atomic-updates': 'off',
|
||||
/**
|
||||
* Allow `async` functions without `await`.
|
||||
* @remarks Originally, this was to be more clear about the return value of a function, but with
|
||||
@@ -170,26 +148,18 @@ export default [
|
||||
* `return await somePromise` have their own use-cases.
|
||||
*/
|
||||
'require-await': 'off',
|
||||
'semi': [2, 'always'],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [
|
||||
2,
|
||||
{
|
||||
words: true,
|
||||
nonwords: false,
|
||||
},
|
||||
],
|
||||
'unicorn/prefer-node-protocol': 'warn'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
...mochaPlugin.configs.recommended,
|
||||
name: 'Test Files',
|
||||
files: ['**/test/**', '*.spec.*js', '-specs.*js', '*.spec.ts'],
|
||||
plugins: {
|
||||
mocha: mochaPlugin,
|
||||
},
|
||||
extends: [mochaPlugin.configs.recommended],
|
||||
rules: {
|
||||
...mochaPlugin.configs.recommended.rules,
|
||||
/**
|
||||
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
|
||||
* @remarks We often test things which explicitly violate types.
|
||||
@@ -202,27 +172,21 @@ export default [
|
||||
*/
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import-x/no-named-as-default-member': 'off',
|
||||
'mocha/consistent-spacing-between-blocks': 'off',
|
||||
'mocha/max-top-level-suites': 'off',
|
||||
'mocha/no-exclusive-tests': 2,
|
||||
'mocha/no-exclusive-tests': 'error',
|
||||
'mocha/no-exports': 'off',
|
||||
'mocha/no-mocha-arrows': 2,
|
||||
'mocha/no-pending-tests': 'off',
|
||||
'mocha/no-setup-in-describe': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Ignores',
|
||||
ignores: [
|
||||
...(fs.existsSync(gitignorePath) ? includeIgnoreFile(gitignorePath).ignores : []),
|
||||
'**/.*',
|
||||
'**/*-d.ts',
|
||||
'**/*.min.js',
|
||||
'**/build/**',
|
||||
'**/coverage/**',
|
||||
],
|
||||
}
|
||||
|
||||
];
|
||||
fs.existsSync(gitignorePath) ? includeIgnoreFile(gitignorePath) : {},
|
||||
globalIgnores([
|
||||
'**/.*',
|
||||
'**/*-d.ts',
|
||||
'**/*.min.js',
|
||||
'**/build/**',
|
||||
'**/coverage/**',
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@appium/eslint-config-appium-ts",
|
||||
"type": "module",
|
||||
"version": "2.0.5",
|
||||
"version": "3.0.0",
|
||||
"description": "Shared ESLint config for Appium projects (TypeScript version)",
|
||||
"keywords": [
|
||||
"eslint",
|
||||
@@ -29,14 +29,17 @@
|
||||
"test:smoke": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/compat": "2.0.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/compat": "2.0.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@stylistic/eslint-plugin": "5.7.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import-x": "4.16.1",
|
||||
"eslint-plugin-mocha": "11.2.0",
|
||||
"eslint-plugin-n": "17.23.2",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"typescript-eslint": "8.48.1"
|
||||
"eslint-plugin-unicorn": "62.0.0",
|
||||
"typescript-eslint": "8.53.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.0.0"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user