mirror of
https://github.com/appium/appium.git
synced 2026-10-31 22:32:24 +08:00
Compare commits
143
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 | ||
|
|
9004554879 | ||
|
|
86d84b8c6a | ||
|
|
24b54f4e89 | ||
|
|
65bacd51db | ||
|
|
5c1b043ddd | ||
|
|
2676dd64f0 | ||
|
|
a5c786fe84 | ||
|
|
e20ace541f | ||
|
|
9a0cb698bd | ||
|
|
7e8cc784c8 | ||
|
|
69756995c2 | ||
|
|
49f8f8a073 | ||
|
|
83e1cc4e1d | ||
|
|
65bd6faa4a | ||
|
|
901e71049c | ||
|
|
ecbc7c4879 | ||
|
|
d1eb6856ba | ||
|
|
d81e682243 | ||
|
|
991b4baee2 | ||
|
|
54e8ef7764 | ||
|
|
af2701ac4d | ||
|
|
2235689d93 | ||
|
|
c4c895cbdd | ||
|
|
18674037bc | ||
|
|
8d0f9017c4 | ||
|
|
aa6a8531e6 | ||
|
|
65a17a1b59 | ||
|
|
c23e45b09d | ||
|
|
82536ae9d5 | ||
|
|
1034b02925 | ||
|
|
2fd9cfb6d5 | ||
|
|
edceebe78d | ||
|
|
73cff7dd31 | ||
|
|
f12fc21dcb | ||
|
|
4658cf60ce | ||
|
|
18089f70e4 | ||
|
|
a7d19f481f | ||
|
|
9decd8b00c | ||
|
|
968460c1a9 | ||
|
|
39cca27784 | ||
|
|
1c0a07c19f | ||
|
|
2ceb39df06 | ||
|
|
cabe2e53bc | ||
|
|
9c43921ac9 | ||
|
|
795181ffc2 | ||
|
|
f01b6fb054 | ||
|
|
a09c85e59e | ||
|
|
050bad8f12 | ||
|
|
38c918711a | ||
|
|
982d33e318 | ||
|
|
46bd0512d7 | ||
|
|
4d8189d68a | ||
|
|
e357a55ebc | ||
|
|
1dff1300e6 |
@@ -15,10 +15,6 @@ inputs:
|
||||
description: Whether to run npm run build
|
||||
required: false
|
||||
default: 'true'
|
||||
clear-v8-cache:
|
||||
description: Whether to clear V8 code cache (for Node.js 24+ compatibility)
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -28,23 +24,18 @@ runs:
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: SocketDev Security Check
|
||||
uses: SocketDev/action@v1
|
||||
with:
|
||||
useRollingCache: true
|
||||
install-command: ${{ inputs.install-command }}
|
||||
mode: firewall-free
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
# Replace npm with sfw npm in the install command
|
||||
install_cmd="${{ inputs.install-command }}"
|
||||
install_cmd="${install_cmd//npm /sfw npm }"
|
||||
$install_cmd
|
||||
- name: Build packages
|
||||
if: ${{ inputs.run-build == 'true' }}
|
||||
shell: bash
|
||||
run: npm run build
|
||||
- name: Clear V8 code cache
|
||||
if: ${{ inputs.clear-v8-cache == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
# Clear V8 code cache to avoid bytecode deserialization errors
|
||||
if [[ -d "$HOME/.node" ]]; then
|
||||
rm -rf "$HOME/.node"/* 2>/dev/null || true
|
||||
fi
|
||||
if [[ -d "$HOME/.cache/node" ]]; then
|
||||
rm -rf "$HOME/.cache/node"/* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
@@ -54,20 +54,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
prepare_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
versions: ${{ steps.generate-matrix.outputs.lts }}
|
||||
steps:
|
||||
- name: Select all current LTS versions of Node.js
|
||||
id: generate-matrix
|
||||
uses: msimerson/node-lts-versions@v1
|
||||
uses: appium/appium-workflows/.github/workflows/node-lts-matrix.yml@main
|
||||
|
||||
prepare_e2e_packages:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.discover-packages.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Discover packages with e2e tests
|
||||
id: discover-packages
|
||||
run: |
|
||||
@@ -116,14 +110,13 @@ jobs:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
install-command: npm ci --foreground-scripts
|
||||
run-build: 'true'
|
||||
clear-v8-cache: 'true'
|
||||
- name: Run smoke tests
|
||||
run: npm run test:smoke
|
||||
|
||||
@@ -136,14 +129,13 @@ jobs:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
install-command: npm ci --foreground-scripts
|
||||
run-build: 'true'
|
||||
clear-v8-cache: 'true'
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
@@ -156,14 +148,13 @@ jobs:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
install-command: npm ci --foreground-scripts
|
||||
run-build: 'true'
|
||||
clear-v8-cache: 'true'
|
||||
- name: Run type tests
|
||||
run: npm run test:types
|
||||
|
||||
@@ -180,14 +171,13 @@ jobs:
|
||||
package_dir: ${{ fromJSON(needs.prepare_e2e_packages.outputs.packages) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
install-command: npm ci --foreground-scripts
|
||||
run-build: 'true'
|
||||
clear-v8-cache: 'true'
|
||||
- name: Run E2E tests for ${{ matrix.package_dir }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
@@ -198,13 +188,12 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup and install (no build)
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: lts/*
|
||||
install-command: npm ci
|
||||
run-build: 'false'
|
||||
clear-v8-cache: 'false'
|
||||
- name: ESLint
|
||||
run: npm run lint:ci
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
project_name: appium-inspector
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
@@ -12,7 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -27,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.8
|
||||
uses: peter-evans/create-pull-request@v8.1.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies (Node.js)
|
||||
uses: bahmutov/npm-install@v1
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
env:
|
||||
FOSSA_API_KEY: ${{secrets.FOSSA_API_KEY}}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: master
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
@@ -4,12 +4,7 @@ on:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
conventional:
|
||||
name: PR Title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: beemojs/conventional-pr-action@v3
|
||||
with:
|
||||
config-preset: angular
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
lint:
|
||||
uses: appium/appium-workflows/.github/workflows/pr-title.yml@main
|
||||
with:
|
||||
config-preset: angular
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
name: Check Renovate Configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Validate Renovate Config
|
||||
uses: rinchsan/renovate-config-validator@v0.2.0
|
||||
with:
|
||||
|
||||
+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
+3124
-18446
File diff suppressed because it is too large
Load Diff
+28
-29
@@ -85,75 +85,74 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@tsconfig/node20": "20.1.6",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@stylistic/eslint-plugin": "5.7.1",
|
||||
"@tsconfig/node20": "20.1.8",
|
||||
"@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",
|
||||
"@types/chai": "5.2.3",
|
||||
"@types/chai-as-promised": "8.0.2",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/jsftp": "2.1.5",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/klaw": "3.0.7",
|
||||
"@types/lockfile": "1.0.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@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.0",
|
||||
"@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": "17.0.4",
|
||||
"@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.34",
|
||||
"asyncbox": "3.0.0",
|
||||
"chai": "6.2.0",
|
||||
"@types/yargs": "17.0.35",
|
||||
"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-mocha": "10.5.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.0",
|
||||
"get-port": "5.1.1",
|
||||
"finalhandler": "2.1.1",
|
||||
"get-port": "7.1.0",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "9.0.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"lerna": "9.0.3",
|
||||
"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.6.2",
|
||||
"prettier": "3.8.1",
|
||||
"rewiremock": "3.14.6",
|
||||
"rimraf": "6.1.0",
|
||||
"serve-static": "2.2.0",
|
||||
"sinon": "21.0.0",
|
||||
"rimraf": "6.1.2",
|
||||
"serve-static": "2.2.1",
|
||||
"sinon": "21.0.1",
|
||||
"sinon-chai": "4.0.1",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"teen_process": "3.0.2",
|
||||
"teen_process": "4.0.8",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.33.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.4",
|
||||
"webdriverio": "9.20.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,30 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.1](https://github.com/appium/appium/compare/appium@3.1.0...appium@3.1.1) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
authors:
|
||||
- sai
|
||||
- srini
|
||||
date: 2025-11-25
|
||||
---
|
||||
|
||||
# Announcing LambdaTest as Appium's Newest Development Partner
|
||||
|
||||
As part of our ongoing effort to recognize organizations that meaningfully support the Appium project through dedicated maintainer time, we are excited to welcome our newest Development Partner: LambdaTest!
|
||||
LambdaTest has long been an active participant in the automated testing ecosystem. Today, they are officially formalizing their commitment by investing people—dedicated engineers who will directly support Appium's development, maintenance, and ecosystem growth.
|
||||
This kind of hands-on contribution is crucial for ensuring Appium continues to evolve. We deeply value partners who empower their engineering teams to contribute code directly to open source rather than just sponsorship.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
<div style="text-align:center;width:100%">
|
||||
<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="300" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="300" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
LambdaTest engineers are already contributing actively across several key Appium repositories. Their current focus includes:
|
||||
**appium-mcp** – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
**appium-remote-xpc** – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
**appium-tun-tap** – Assisting in the development and maintenance of network virtualization and tunneling components for advanced testing scenarios.
|
||||
|
||||
These contributions strengthen critical infrastructure within the Appium ecosystem and demonstrate LambdaTest's commitment to deep technical involvement in the project's evolution.
|
||||
About LambdaTest
|
||||
|
||||
LambdaTest’s platform already serves a massive community of Appium users. As a GenAI-powered Quality Engineering Platform, they provide a full-stack testing cloud with 10,000+ real mobile devices and 3,000+ browsers.
|
||||
Beyond standard infrastructure, they are pushing the boundaries of test execution with AI-native test management and next-generation agents like KaneAI. By supporting Appium alongside Selenium, Playwright, and other major frameworks, LambdaTest helps teams orchestrate intelligent automation at cloud scale.
|
||||
|
||||
LambdaTest will continue contributing people, code, and development expertise to strengthen Appium's core functionality. Their commitment represents a significant investment into the future of the framework, and we're thrilled to have them collaborating with us at this deeper level.
|
||||
The Appium project is proud to recognize LambdaTest as our newest official Development Partner. We can't wait to see what we'll build together!
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Appium Server Security
|
||||
|
||||
The Appium team makes every effort to ensure the security of the Appium server. This is especially
|
||||
important when Appium is run in a multitenant environment, or when multiple users are running
|
||||
sessions on the same Appium server. In general, you can only safely enable all Appium's features if
|
||||
sessions on the same Appium server. In general, you can only safely enable all of Appium's features if
|
||||
all the following are true:
|
||||
|
||||
- You're running your own Appium server locally or within a protected internal network
|
||||
@@ -12,18 +12,18 @@ all the following are true:
|
||||
- You don't expose Appium's port(s) to the wider internet
|
||||
|
||||
But because many Appium users might not be able to guarantee such a safe environment, the Appium
|
||||
team puts many features behind a security protection mechanism which forces system admins (the
|
||||
people that are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
team puts many features behind a security protection mechanism, which forces system admins (the
|
||||
people who are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
(Third-party driver and plugin authors can also [hide behaviour behind security
|
||||
flags](../developing/build-drivers.md).)
|
||||
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities;
|
||||
this is the responsibility of the server admin who configures and launches the Appium server.
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities.
|
||||
This is the responsibility of the server admin who configures and launches the Appium server.
|
||||
|
||||
## Security Server Args
|
||||
|
||||
The [Server CLI](../reference/cli/server.md) doc outlines three relevant arguments which may be passed to
|
||||
Appium when starting it from the command line:
|
||||
Appium, when starting it from the command line:
|
||||
|
||||
|<div style="width:10em">Parameter</div>|Description|
|
||||
|---------------------------------------|-----------|
|
||||
@@ -44,7 +44,7 @@ to the `bar` feature for all drivers.
|
||||
|
||||
## Insecure Features
|
||||
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus,
|
||||
you should read through the documentation for a particular driver to know which feature names it
|
||||
might use. Here is an incomplete list of examples from some of Appium's official drivers:
|
||||
|
||||
@@ -53,10 +53,10 @@ might use. Here is an incomplete list of examples from some of Appium's official
|
||||
|`get_server_logs`|Allows retrieving of Appium server logs via the Webdriver log interface|IOS, XCUITest, Android, UiAutomator2, Espresso|
|
||||
|`adb_shell`|Allows execution of arbitrary shell commands via ADB, using the `mobile: shell` command|Android, UiAutomator2, Espresso|
|
||||
|`record_audio`|Allow recording of host machine audio inputs|XCUITest|
|
||||
|`execute_driver_script`| Allows to send a request which has multiple Appium commands.|Execute Driver Plugin|
|
||||
|`execute_driver_script`| Allows sending a request that has multiple Appium commands.|Execute Driver Plugin|
|
||||
|
||||
Some insecure features operate on the server level, and do not require a driver session. Enabling
|
||||
these features requires using the wildcard prefix:
|
||||
Some insecure features operate on the server level and do not require a driver session. Enabling
|
||||
these features require using the wildcard prefix:
|
||||
|
||||
|<div style="width:12em">Feature Name</div>|Description|
|
||||
|------------|-----------|
|
||||
@@ -73,17 +73,23 @@ appium --allow-insecure=first:foo
|
||||
Turn on the `foo` feature for all drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo
|
||||
appium --allow-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on the `foo` feature for all drivers _except_ `first`:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo --deny-insecure=first:foo
|
||||
appium --allow-insecure='*:foo' --deny-insecure=first:foo
|
||||
```
|
||||
|
||||
Turn on all features _except_ `foo` for all drivers:
|
||||
|
||||
```bash
|
||||
appium --relaxed-security --deny-insecure=*:foo
|
||||
appium --relaxed-security --deny-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on multiple insecure features for specific drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=uiautomator2:adb_shell,xcuitest:get_server_logs
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
authors:
|
||||
- sai
|
||||
- srini
|
||||
date: 2025-11-25
|
||||
---
|
||||
|
||||
# Announcing LambdaTest as Appium's Newest Development Partner
|
||||
|
||||
As part of our ongoing effort to recognize organizations that meaningfully support the Appium project through dedicated maintainer time, we are excited to welcome our newest Development Partner: LambdaTest!
|
||||
LambdaTest has long been an active participant in the automated testing ecosystem. Today, they are officially formalizing their commitment by investing people—dedicated engineers who will directly support Appium's development, maintenance, and ecosystem growth.
|
||||
This kind of hands-on contribution is crucial for ensuring Appium continues to evolve. We deeply value partners who empower their engineering teams to contribute code directly to open source rather than just sponsorship.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
<div style="text-align:center;width:100%"><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="300" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="300" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
LambdaTest engineers are already contributing actively across several key Appium repositories. Their current focus includes:
|
||||
**appium-mcp** – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
**appium-remote-xpc** – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
**appium-tun-tap** – Assisting in the development and maintenance of network virtualization and tunneling components for advanced testing scenarios.
|
||||
|
||||
These contributions strengthen critical infrastructure within the Appium ecosystem and demonstrate LambdaTest's commitment to deep technical involvement in the project's evolution.
|
||||
About LambdaTest
|
||||
|
||||
LambdaTest’s platform already serves a massive community of Appium users. As a GenAI-powered Quality Engineering Platform, they provide a full-stack testing cloud with 10,000+ real mobile devices and 3,000+ browsers.
|
||||
Beyond standard infrastructure, they are pushing the boundaries of test execution with AI-native test management and next-generation agents like KaneAI. By supporting Appium alongside Selenium, Playwright, and other major frameworks, LambdaTest helps teams orchestrate intelligent automation at cloud scale.
|
||||
|
||||
LambdaTest will continue contributing people, code, and development expertise to strengthen Appium's core functionality. Their commitment represents a significant investment into the future of the framework, and we're thrilled to have them collaborating with us at this deeper level.
|
||||
The Appium project is proud to recognize LambdaTest as our newest official Development Partner. We can't wait to see what we'll build together!
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Appium Server Security
|
||||
|
||||
The Appium team makes every effort to ensure the security of the Appium server. This is especially
|
||||
important when Appium is run in a multitenant environment, or when multiple users are running
|
||||
sessions on the same Appium server. In general, you can only safely enable all Appium's features if
|
||||
sessions on the same Appium server. In general, you can only safely enable all of Appium's features if
|
||||
all the following are true:
|
||||
|
||||
- You're running your own Appium server locally or within a protected internal network
|
||||
@@ -12,18 +12,18 @@ all the following are true:
|
||||
- You don't expose Appium's port(s) to the wider internet
|
||||
|
||||
But because many Appium users might not be able to guarantee such a safe environment, the Appium
|
||||
team puts many features behind a security protection mechanism which forces system admins (the
|
||||
people that are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
team puts many features behind a security protection mechanism, which forces system admins (the
|
||||
people who are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
(Third-party driver and plugin authors can also hide behaviour behind security
|
||||
flags.)
|
||||
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities;
|
||||
this is the responsibility of the server admin who configures and launches the Appium server.
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities.
|
||||
This is the responsibility of the server admin who configures and launches the Appium server.
|
||||
|
||||
## Security Server Args
|
||||
|
||||
The [Server CLI](../reference/cli/server.md) doc outlines three relevant arguments which may be passed to
|
||||
Appium when starting it from the command line:
|
||||
Appium, when starting it from the command line:
|
||||
|
||||
| <div style="width:10em">Parameter</div> | Description |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -44,7 +44,7 @@ to the `bar` feature for all drivers.
|
||||
|
||||
## Insecure Features
|
||||
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus,
|
||||
you should read through the documentation for a particular driver to know which feature names it
|
||||
might use. Here is an incomplete list of examples from some of Appium's official drivers:
|
||||
|
||||
@@ -53,10 +53,10 @@ might use. Here is an incomplete list of examples from some of Appium's official
|
||||
| `get_server_logs` | Allows retrieving of Appium server logs via the Webdriver log interface | IOS, XCUITest, Android, UiAutomator2, Espresso |
|
||||
| `adb_shell` | Allows execution of arbitrary shell commands via ADB, using the `mobile: shell` command | Android, UiAutomator2, Espresso |
|
||||
| `record_audio` | Allow recording of host machine audio inputs | XCUITest |
|
||||
| `execute_driver_script` | Allows to send a request which has multiple Appium commands. | Execute Driver Plugin |
|
||||
| `execute_driver_script` | Allows sending a request that has multiple Appium commands. | Execute Driver Plugin |
|
||||
|
||||
Some insecure features operate on the server level, and do not require a driver session. Enabling
|
||||
these features requires using the wildcard prefix:
|
||||
Some insecure features operate on the server level and do not require a driver session. Enabling
|
||||
these features require using the wildcard prefix:
|
||||
|
||||
| <div style="width:12em">Feature Name</div> | Description |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
@@ -73,17 +73,23 @@ appium --allow-insecure=first:foo
|
||||
Turn on the `foo` feature for all drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo
|
||||
appium --allow-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on the `foo` feature for all drivers _except_ `first`:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo --deny-insecure=first:foo
|
||||
appium --allow-insecure='*:foo' --deny-insecure=first:foo
|
||||
```
|
||||
|
||||
Turn on all features _except_ `foo` for all drivers:
|
||||
|
||||
```bash
|
||||
appium --relaxed-security --deny-insecure=*:foo
|
||||
appium --relaxed-security --deny-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on multiple insecure features for specific drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=uiautomator2:adb_shell,xcuitest:get_server_logs
|
||||
```
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
authors:
|
||||
- sai
|
||||
- srini
|
||||
date: 2025-11-25
|
||||
---
|
||||
|
||||
# Announcing LambdaTest as Appium's Newest Development Partner
|
||||
|
||||
As part of our ongoing effort to recognize organizations that meaningfully support the Appium project through dedicated maintainer time, we are excited to welcome our newest Development Partner: LambdaTest!
|
||||
LambdaTest has long been an active participant in the automated testing ecosystem. Today, they are officially formalizing their commitment by investing people—dedicated engineers who will directly support Appium's development, maintenance, and ecosystem growth.
|
||||
This kind of hands-on contribution is crucial for ensuring Appium continues to evolve. We deeply value partners who empower their engineering teams to contribute code directly to open source rather than just sponsorship.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
<div style="text-align:center;width:100%"><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="300" />
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-lambdatest-light.png#only-light" width="300" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
LambdaTest engineers are already contributing actively across several key Appium repositories. Their current focus includes:
|
||||
**appium-mcp** – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
**appium-remote-xpc** – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
**appium-tun-tap** – Assisting in the development and maintenance of network virtualization and tunneling components for advanced testing scenarios.
|
||||
|
||||
These contributions strengthen critical infrastructure within the Appium ecosystem and demonstrate LambdaTest's commitment to deep technical involvement in the project's evolution.
|
||||
About LambdaTest
|
||||
|
||||
LambdaTest’s platform already serves a massive community of Appium users. As a GenAI-powered Quality Engineering Platform, they provide a full-stack testing cloud with 10,000+ real mobile devices and 3,000+ browsers.
|
||||
Beyond standard infrastructure, they are pushing the boundaries of test execution with AI-native test management and next-generation agents like KaneAI. By supporting Appium alongside Selenium, Playwright, and other major frameworks, LambdaTest helps teams orchestrate intelligent automation at cloud scale.
|
||||
|
||||
LambdaTest will continue contributing people, code, and development expertise to strengthen Appium's core functionality. Their commitment represents a significant investment into the future of the framework, and we're thrilled to have them collaborating with us at this deeper level.
|
||||
The Appium project is proud to recognize LambdaTest as our newest official Development Partner. We can't wait to see what we'll build together!
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Appium Server Security
|
||||
|
||||
The Appium team makes every effort to ensure the security of the Appium server. This is especially
|
||||
important when Appium is run in a multitenant environment, or when multiple users are running
|
||||
sessions on the same Appium server. In general, you can only safely enable all Appium's features if
|
||||
sessions on the same Appium server. In general, you can only safely enable all of Appium's features if
|
||||
all the following are true:
|
||||
|
||||
- You're running your own Appium server locally or within a protected internal network
|
||||
@@ -12,18 +12,18 @@ all the following are true:
|
||||
- You don't expose Appium's port(s) to the wider internet
|
||||
|
||||
But because many Appium users might not be able to guarantee such a safe environment, the Appium
|
||||
team puts many features behind a security protection mechanism which forces system admins (the
|
||||
people that are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
team puts many features behind a security protection mechanism, which forces system admins (the
|
||||
people who are in charge of starting the Appium server) to _explicitly opt-in_ to these features.
|
||||
(Third-party driver and plugin authors can also hide behaviour behind security
|
||||
flags.)
|
||||
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities;
|
||||
this is the responsibility of the server admin who configures and launches the Appium server.
|
||||
For security reasons, Appium client sessions can _not_ request feature enablement via capabilities.
|
||||
This is the responsibility of the server admin who configures and launches the Appium server.
|
||||
|
||||
## Security Server Args
|
||||
|
||||
The [Server CLI](../reference/cli/server.md) doc outlines three relevant arguments which may be passed to
|
||||
Appium when starting it from the command line:
|
||||
Appium, when starting it from the command line:
|
||||
|
||||
| <div style="width:10em">Parameter</div> | Description |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -44,7 +44,7 @@ to the `bar` feature for all drivers.
|
||||
|
||||
## Insecure Features
|
||||
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus
|
||||
Each Appium driver is responsible for its own security, and can create its own feature names. Thus,
|
||||
you should read through the documentation for a particular driver to know which feature names it
|
||||
might use. Here is an incomplete list of examples from some of Appium's official drivers:
|
||||
|
||||
@@ -53,10 +53,10 @@ might use. Here is an incomplete list of examples from some of Appium's official
|
||||
| `get_server_logs` | Allows retrieving of Appium server logs via the Webdriver log interface | IOS, XCUITest, Android, UiAutomator2, Espresso |
|
||||
| `adb_shell` | Allows execution of arbitrary shell commands via ADB, using the `mobile: shell` command | Android, UiAutomator2, Espresso |
|
||||
| `record_audio` | Allow recording of host machine audio inputs | XCUITest |
|
||||
| `execute_driver_script` | Allows to send a request which has multiple Appium commands. | Execute Driver Plugin |
|
||||
| `execute_driver_script` | Allows sending a request that has multiple Appium commands. | Execute Driver Plugin |
|
||||
|
||||
Some insecure features operate on the server level, and do not require a driver session. Enabling
|
||||
these features requires using the wildcard prefix:
|
||||
Some insecure features operate on the server level and do not require a driver session. Enabling
|
||||
these features require using the wildcard prefix:
|
||||
|
||||
| <div style="width:12em">Feature Name</div> | Description |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
@@ -73,17 +73,23 @@ appium --allow-insecure=first:foo
|
||||
Turn on the `foo` feature for all drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo
|
||||
appium --allow-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on the `foo` feature for all drivers _except_ `first`:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=*:foo --deny-insecure=first:foo
|
||||
appium --allow-insecure='*:foo' --deny-insecure=first:foo
|
||||
```
|
||||
|
||||
Turn on all features _except_ `foo` for all drivers:
|
||||
|
||||
```bash
|
||||
appium --relaxed-security --deny-insecure=*:foo
|
||||
appium --relaxed-security --deny-insecure='*:foo'
|
||||
```
|
||||
|
||||
Turn on multiple insecure features for specific drivers:
|
||||
|
||||
```bash
|
||||
appium --allow-insecure=uiautomator2:adb_shell,xcuitest:get_server_logs
|
||||
```
|
||||
|
||||
@@ -1,200 +1,91 @@
|
||||
---
|
||||
title: How Does Appium Work?
|
||||
title: Appium 如何工作?
|
||||
---
|
||||
|
||||
As mentioned on the main page, Appium is an open-source project and ecosystem of related software,
|
||||
designed to facilitate UI automation of many app platforms. With the release of Appium 2, Appium
|
||||
has the following primary goals:[^1]
|
||||
正如主页所述,Appium 是一个开源项目和相关软件生态系统,旨在促进多种应用平台的 UI 自动化。 随着 Appium 2 的发布,Appium 具有以下主要目标:[^1]
|
||||
|
||||
- Make platform-specific automation capabilities available under a cross-platform, standard API
|
||||
- Allow easy access to this API from any programming language
|
||||
- Provide tools to enable convenient community development of Appium extensions
|
||||
- 在跨平台标准 API 下提供特定于平台的自动化功能
|
||||
- 允许从任何编程语言轻松访问此 API
|
||||
- 提供工具以便社区方便地开发 Appium 扩展
|
||||
|
||||
[^1]: To meet these primary goals, we also work with a set of secondary goals or methodology
|
||||
principles, which we also encourage for Appium extension developers:
|
||||
[^1]: 为实现这些主要目标,我们还遵循一套次要目标或方法原则,我们也鼓励 Appium 扩展开发者遵循这些原则:
|
||||
|
||||
- As far as possible, rely on (and contribute to) open source technology
|
||||
- As far as possible, rely on vendor-provided tools for a given platform
|
||||
- As far as possible, rely on automation tools that allow automation of unmodified apps (prefer
|
||||
not to require the user to build in additional SDKs or software that introduce discrepancies
|
||||
between the test version of the app and the production version)
|
||||
- As far as possible, rely on existing standards instead of creating new ones
|
||||
- 尽可能依赖(并贡献于)开源技术
|
||||
- 尽可能依赖给定平台的供应商提供的工具
|
||||
- 尽可能依赖不修改应用就能实现自动化(最好不要要求用户构建额外的 SDK 或软件,从而在测试版本和生产版本之间引入差异)的自动化工具
|
||||
- 尽可能依赖现有标准而不是创建新标准
|
||||
|
||||
So, take any app platform you know about, like iOS or Android. Appium wants for there to be a way
|
||||
for developers and testers to write UI automation code for that platform, according to a single,
|
||||
unified API. Based on Appium's goals, we have a lot of questions to answer to make it all work:
|
||||
所以,以您了解的任何应用平台为例,如 iOS 或 Android。 Appium 希望开发者测试人员能够为该平台编写 UI 自动化代码,根据单一的统一 API。 根据 Appium 的目标,我们有很多问题需要回答以使其全部工作:
|
||||
|
||||
- Which API should that "single, unified" API be?
|
||||
- How do we map that API to automation behaviour for a specific platform?
|
||||
- How do we make that API accessible via multiple popular programming languages?
|
||||
- “单一统一”的 API 应该是什么?
|
||||
- 我们如何将此 API 映射到特定平台的自动化行为?
|
||||
- 我们如何使此 API 通过多种流行编程语言访问?
|
||||
|
||||
There's another, larger, question lurking in the background here too, given that there are more app
|
||||
platforms out there than just iOS and Android:
|
||||
这里还有另一个更大的问题,鉴于除了 iOS 和 Android 之外还有更多应用平台:
|
||||
|
||||
- How do we enable automation for _all_ the platforms?
|
||||
- 我们如何为_所有_平台启用自动化?
|
||||
|
||||
Exploring Appium's answers to these questions may not be the quickest way to learn what Appium is,
|
||||
but it is certainly a good one! So let's dive in.
|
||||
探索 Appium 对这些问题的答案可能不是学习 Appium 是什么的最快方式,但它肯定是一个好方法! 所以让我们深入了解。
|
||||
|
||||
## Appium's choice of API
|
||||
## Appium 的 API 选择
|
||||
|
||||
Appium is very fortunate to have been preceded by a technology which has been a long-standing
|
||||
pioneer in the field of UI automation, namely [Selenium](https://selenium.dev). The goal of the
|
||||
Selenium project has been to support UI automation of web browsers, and in this way we can think of
|
||||
it as occupying a subset of Appium's goals. Along the way, Selenium (and, after they merged,
|
||||
another project called WebDriver) developed a relatively stable API for browser automation.
|
||||
Appium 非常幸运地继承了一个在 UI 自动化领域长期领先的技术,即 [Selenium](https://selenium.dev)。 Selenium 项目的目标是支持 Web 浏览器的 UI 自动化,因此我们可以将其视为占据 Appium 目标子集的一部分。 在此过程中,Selenium(以及后来合并的另一个名为 WebDriver 的项目)开发了一个相对稳定的浏览器自动化 API。
|
||||
|
||||
Over the years, Selenium worked with various web browser vendors and the [W3C](https://w3.org)
|
||||
standards group to turn its API into an official web browser standard, called the WebDriver
|
||||
specification. All the main browsers now
|
||||
implement automation capabilities inline with the WebDriver spec, without the Selenium team having
|
||||
to maintain any software that performs actual automation; standards for the win!
|
||||
多年来,Selenium 与各种 Web 浏览器供应商和 [W3C](https://w3.org) 标准小组合作,将其 API 转变为官方 Web 浏览器标准,称为 [WebDriver 规范](https://w3c.github.io/webdriver/)。
|
||||
所有主要浏览器现在都实现了符合 WebDriver 规范的自动化功能,而无需 Selenium 团队维护任何执行实际自动化的软件;标准获胜!
|
||||
|
||||
Appium's initial goals were to develop an automation standard for mobile apps (iOS and Android). We
|
||||
could have made up something new, but in the spirit of joining forces and keeping standards, well,
|
||||
standard, we decided to adopt the WebDriver spec as Appium's API.[^2] While user interaction on
|
||||
websites and in mobile native apps are not entirely identical (with even greater differences once
|
||||
we start to consider, for example, TV platforms controlled by simple remotes), the fact is that
|
||||
most software UIs are pretty much the same. This means that the WebDriver spec provides automation
|
||||
API primitives (finding elements, interacting with elements, loading pages or screens, etc...) that
|
||||
more or less map to any platform.
|
||||
Appium 的最初目标是为移动应用(iOS 和 Android)开发自动化标准。 我们本可以发明一些新的东西,但为了团结力量并保持标准的标准化,我们决定采用 WebDriver 规范作为 Appium 的 API。[^2] 虽然网站和移动原生应用的用户交互并不完全相同(一旦我们开始考虑例如由简单遥控器控制的电视平台,差异会更大),但事实是大多数软件 UI 都非常相似。 这意味着 WebDriver 规范提供了自动化 API 原语(查找元素、与元素交互、加载页面或屏幕等) 这些原语或多或少映射到任何平台。
|
||||
|
||||
[^2]: Technically, when Appium was first written, we were dealing with something older than the
|
||||
WebDriver spec, called the JSON Wire Protocol. Since then, Appium continued to evolve along with
|
||||
the W3C spec and is fully W3C-compliant.
|
||||
[^2]: 从技术上讲,当 Appium 首次编写时,我们处理的是比 WebDriver 规范更古老的东西,称为 JSON Wire Protocol。 从那时起,Appium 继续与 W3C 规范一起演化,并完全符合 W3C 规范。
|
||||
|
||||
Of course, Appium wants to support the cases where user interaction _does_ differ from web to
|
||||
mobile or web to TV, and so Appium also makes use of the built-in _extensibility_ of the WebDriver
|
||||
spec. The result is that, no matter what platform you want to automate, when you use Appium, you
|
||||
will do so using the standard WebDriver spec, with two caveats:
|
||||
当然,Appium 希望支持用户交互从 Web 到移动或 Web 到电视确实不同的情况,因此 Appium 还利用 WebDriver 规范的内置_可扩展性_。 结果是,无论您想要自动化哪个平台,当您使用 Appium 时,您将使用标准 WebDriver 规范,但有两个注意事项:
|
||||
|
||||
- We might not have any way to support a particular WebDriver API command on a given platform, and
|
||||
so some commands might be unsupported (for example, getting or setting cookies is not possible in
|
||||
the world of native mobile app automation).
|
||||
- We might support automation behaviours that go _beyond_ what's available in the WebDriver API
|
||||
command list, though any such commands will be valid and spec-compliant extensions to the
|
||||
WebDriver API.
|
||||
- 我们可能无法在给定平台上支持特定的 WebDriver API 命令,因此某些命令可能不受支持(例如,在原生移动应用自动化世界中,获取或设置 cookie 是不可能的)。
|
||||
- 我们可能支持超出 WebDriver API 命令列表的自动化行为,尽管任何此类命令将是有效的、符合规范的 WebDriver API 扩展。
|
||||
|
||||
How do you actually _use_ the WebDriver API, particularly in the context of Appium? We'll cover
|
||||
that in the [section below](#universal-programming-language-access) on how Appium provides
|
||||
universal programming language access. All you need to know for now is that the way Appium
|
||||
introduces a universal UI automation interface is by implementing the WebDriver protocol.
|
||||
您实际上如何_使用_ WebDriver API,特别是 Appium 的背景下? 我们将在下面的[部分](#universal-programming-language-access)中介绍 Appium 如何提供通用编程语言访问。 您现在只需要知道 Appium 通过实现 WebDriver 协议来引入通用 UI 自动化接口。
|
||||
|
||||
## Platform automation behaviour
|
||||
## 平台自动化行为
|
||||
|
||||
The next question is, how does Appium map this protocol to automation behaviour on a wide range of
|
||||
platforms? The trick is that, strictly speaking, Appium doesn't! It leaves this responsibility up
|
||||
to a kind of software module called an Appium _driver_. There's a whole Driver
|
||||
Introduction which you can read next, so we won't go into huge detail on how they
|
||||
work for now.
|
||||
下一个问题是,Appium 如何将此协议映射到广泛平台的自动化行为? 严格来说,Appium 没有! 它将此责任留给一种称为 Appium _驱动程序_ 的软件模块。 您可以阅读整个[驱动程序](./drivers.md)的介绍,所以我们现在不会深入细节。
|
||||
|
||||
What's important to understand at the moment is that a driver is kind of like a pluggable module
|
||||
for Appium that gives Appium the power to automate a particular platform (or set of platforms,
|
||||
depending on the goal of the driver). At the end of the day, a driver's responsibility is to simply
|
||||
implement an Appium-internal interface representing the WebDriver protocol. How it implements this
|
||||
interface is totally up to the driver, based on its strategy for making automation happen on
|
||||
a specific platform. Typically, and with a lot more complexity and difficulty in the details,
|
||||
a driver does this by relying on platform-specific automation technologies. For example, Apple
|
||||
maintains an iOS automation technology called
|
||||
[XCUITest](https://developer.apple.com/documentation/xctest/user_interface_tests). The Appium
|
||||
driver that supports iOS app automation is called the XCUITest
|
||||
Driver because ultimately what it does is
|
||||
convert the WebDriver protocol to XCUITest library calls.
|
||||
目前重要的是要理解驱动程序有点像 Appium 的可插拔模块,它赋予 Appium 自动化特定平台(或平台集,取决于驱动程序的目标)的能力。 最终,驱动程序的责任只是实现代表 WebDriver 协议的 Appium 内部接口。 它如何实现此接口完全取决于驱动程序,基于其在特定平台上实现自动化的策略。 通常,在细节上具有更多复杂性和难度,驱动程序通过依赖特定于平台的自动化技术来实现这一点。 例如,Apple 维护一种称为 [XCUITest](https://developer.apple.com/documentation/xctest/user_interface_tests) 的 iOS 自动化技术。 支持 iOS 应用自动化的 Appium 驱动程序称为 [XCUITest 驱动程序](https://github.com/appium/appium-xcuitest-driver),因为它最终所做的是将 WebDriver 协议转换为 XCUITest 库调用。
|
||||
|
||||
One of the reasons that drivers are independent, pluggable modules is that they work completely
|
||||
differently from one another. The tools and requirements for building and using drivers for
|
||||
different platforms are completely different. And so Appium lets you use just the drivers that you
|
||||
need for your automation tasks. Choosing drivers and installing them so that you can use them with
|
||||
your Appium instance is so important that Appium has its very own CLI for managing
|
||||
drivers.
|
||||
驱动程序是独立、可插拔模块的原因之一是它们彼此完全不同。 构建和使用不同平台的驱动程序的工具和要求完全不同。 因此,Appium 让您只使用您自动化任务所需的驱动程序。 选择驱动程序并安装它们以便与您的 Appium 实例一起使用非常重要,以至于 Appium 有自己的 [CLI 来管理驱动程序](../reference/cli/extensions.md)。
|
||||
|
||||
So, to answer our original question, the way that Appium provides access to automation capabilities
|
||||
for a given platform is that the Appium team (or anyone else[^3]) writes a _driver_ for that
|
||||
platform, implementing as much or little of the WebDriver protocol as desired. The driver can then
|
||||
be installed by anyone using Appium.
|
||||
因此,为了回答我们最初的问题,Appium 为给定平台提供自动化功能访问的方式是 Appium 团队(或任何人[^3])为该平台编写一个_驱动程序_,实现 WebDriver 协议的尽可能多或少的部分。 然后,任何使用 Appium 的人都可以安装驱动程序。
|
||||
|
||||
[^3]: You can build and share your own drivers! Check out Building
|
||||
Drivers to learn more about how to develop drivers in Node.js
|
||||
that can be used with Appium.
|
||||
[^3]: 您可以构建和分享自己的驱动程序! 查看[构建驱动程序](../developing/build-drivers.md)以了解更多关于如何在 Node.js 中开发可与 Appium 一起使用的驱动程序的信息。
|
||||
|
||||
## Universal programming language access
|
||||
## 通用编程语言访问
|
||||
|
||||
But what does it mean, or look like, to _use_ Appium, anyway? Since Appium is ultimately a Node.js
|
||||
program, it _could_ have looked like importing Appium and its drivers as libraries into your own
|
||||
Node.js programs. But that wouldn't meet Appium's goal of providing automation capabilities to
|
||||
people using any popular programming language.
|
||||
但使用 Appium 意味着什么,或者看起来像什么? 由于 Appium 最终是一个 Node.js 程序,它_可以_看起来像将 Appium 及其驱动程序作为库导入到您自己的 Node.js 程序中。 但这不会满足 Appium 的目标,即为使用任何流行编程语言的人提供自动化功能。
|
||||
|
||||
Luckily, the fact that Appium rode in on Selenium's coattails meant that we had a solution to this
|
||||
problem from day one. You see, the WebDriver specification is actually an HTTP-based protocol,
|
||||
meaning it is designed to be used over a network rather than within the memory of a single program.
|
||||
幸运的是,Appium 继承 Selenium 的遗产意味着我们从一开始就有了这个问题的解决方案。 您看,WebDriver 规范实际上是一个基于 HTTP 的协议,这意味着它被设计为通过网络而不是单个程序的内存使用。
|
||||
|
||||
One of the main benefits of this "client-server" architecture is that it allows the automation
|
||||
implementer (the thing doing the automation, in this case the 'server') to be completely distinct
|
||||
from the automation runner (the thing defining what automation should be done, in what steps,
|
||||
etc..., in this case the 'client'). Basically, all the "hard stuff" (actually figuring out how to
|
||||
make automation happen on a given platform) can be handled in one place by the server, and "thin"
|
||||
client libraries can be written in any programming language which simply encode HTTP requests to
|
||||
the server in language-appropriate way. It's possible, in other words, to bring basic Appium
|
||||
/ WebDriver capabilities to a new programming language relatively easily, assuming high-level HTTP
|
||||
libraries exist, simply by coding up a basic HTTP client in that language.
|
||||
这种“客户端-服务器”架构的主要好处之一是,它允许自动化实现者(执行自动化的事物,在这种情况下是“服务器”)与自动化运行者(定义应该做什么自动化、以什么步骤等的事物,在这种情况下是“客户端”)完全不同。 基本上,所有“困难的东西”(弄清楚如何在给定平台上实现自动化)可以在服务器的一个地方处理,“瘦”客户端库可以在任何编程语言中编写,这些库只需以适合语言的方式编码对服务器的 HTTP 请求。 换句话说,假设存在高级 HTTP 库,只需在该语言中编写基本的 HTTP 客户端,就可以相对容易地将基本的 Appium / WebDriver 功能引入新的编程语言。
|
||||
|
||||
There are a couple important takeaways here for you, the Appium user:
|
||||
这里有几个重要的点供 Appium 用户了解:
|
||||
|
||||
- Appium is an _HTTP server_. It must run as a process on some computer for as long as you want to
|
||||
be able to use it for automation. It must be accessible on the network to whichever computer you
|
||||
want to use to run the automation from (whether that is the same machine or one across the
|
||||
world).
|
||||
- Unless you want to write raw HTTP calls or use cURL, using Appium for automation involves the use
|
||||
of an [Appium Client](clients.md) in the language of your choice. The goal of each of these
|
||||
clients is to encapsulate the WebDriver protocol so that rather than worrying about the protocol
|
||||
itself, you can work with objects and methods that feel idiomatic for your language.
|
||||
- The Appium server and the Appium client do _not_ need to be running on the same computer. You
|
||||
simply need to be able to send HTTP requests from the client to the server over some network.
|
||||
This greatly facilitates the use of cloud providers for Appium, since they can host the Appium
|
||||
server and any related drivers and devices, and all you need to do is point your client script to
|
||||
their secure endpoints.
|
||||
- Appium 是一个_HTTP 服务器_。 只要您想使用它进行自动化,它必须在某台计算机上作为进程运行。 它必须在网络上可访问,以方便您想要运行自动化的任何计算机访问(无论是同一台机器还是世界另一端的机器)。
|
||||
- 除非您想编写原始 HTTP 调用或使用 cURL,否则请使用您选择的语言的 [Appium 客户端](clients.md)进行自动化。 每个客户端的目标是将 WebDriver 协议封装,以便您不必担心协议本身,而是可以使用感觉适合您语言的对象和方法。
|
||||
- Appium 服务器和 Appium 客户端_不需要_在同一台计算机上运行。 您只需能够通过某个网络从客户端向服务器发送 HTTP 请求。
|
||||
这大大促进了云提供商对 Appium 的使用,因为他们可以托管 Appium 服务器以及任何相关驱动程序和设备,您只需将客户端脚本指向他们的安全端点。
|
||||
|
||||
And of course, none of this is about "testing" per se, purely about the use of Appium and its
|
||||
client libraries for automation purposes. If you want to do automation for the purpose of
|
||||
"testing", you'll likely want to enlist the help of test runners, test frameworks, and the like,
|
||||
none of which need be related to Appium; one of the benefits of Appium's "universal accessibility"
|
||||
is that it plays well with whatever set of tools you find most beneficial for your situation.
|
||||
当然,这一切都不是关于“测试”本身,只是出于自动化目的使用 Appium 及其客户端库。 如果您想出于“测试”目的进行自动化,您可能希望利用测试运行器、测试框架等的帮助,这些都不需要与 Appium 相关;Appium“通用可访问性”的好处之一是它与您发现最适合您情况的任何工具集配合良好。
|
||||
|
||||
## Appium's huge scope
|
||||
## Appium 的巨大范围
|
||||
|
||||
Appium's vision (automation of everything under a single API) is huge! Certainly, much bigger than
|
||||
the team of core maintainers for the open source project. So how does Appium hope to achieve this
|
||||
goal? Basically, by empowering the community to develop functionality on top of Appium as
|
||||
a _platform_. This is what we call the Appium "ecosystem".
|
||||
Appium 的愿景(在单一 API 下自动化一切)是巨大的! 当然,比开源项目的核心维护者团队大得多。 那么 Appium 如何希望实现这一目标? 基本上,通过授权社区在 Appium 之上作为_平台_开发功能。 这就是我们所谓的 Appium“生态系统”。
|
||||
|
||||
The Appium team does officially maintain a few drivers itself (for example, the XCUITest driver
|
||||
that we spoke about earlier). But it cannot hope to have the platform-specific expertise or the
|
||||
capacity to maintain drivers for many different platforms. But what we have done, particularly
|
||||
beginning with Appium 2, is to provide tools to empower the community to join in our vision:
|
||||
Appium 团队确实自己维护了一些驱动程序(例如,我们之前讨论的 XCUITest 驱动程序)。 但它无法拥有特定于平台的专业知识或维护许多不同平台的驱动程序的能力。 但我们所做的,特别是从 Appium 2 开始,是提供工具来授权社区加入我们的愿景:
|
||||
|
||||
- Anyone can create a driver simply by creating a Node.js module that conforms to the appropriate
|
||||
conventions and implements any (sub|super)set of the WebDriver protocol. Creating a driver often
|
||||
involves a minimal amount of code because the WebDriver protocol details are abstracted away, and
|
||||
many helper libraries are available---the same libraries that power the Appium team's own
|
||||
drivers.
|
||||
- Sharing drivers with others is easy using the Appium driver CLI. There is no central authority.
|
||||
Anyone can share drivers publicly or privately, for free or for sale. Drivers can be open or
|
||||
closed source (though obviously we appreciate open source!).
|
||||
- 任何人都可以通过创建一个符合适当约定并实现 WebDriver 协议的任何(子|超)集的 Node.js 模块来创建驱动程序。 创建驱动程序通常涉及最少的代码,因为 WebDriver 协议细节被抽象化,许多帮助库可用---这些库为 Appium 团队自己的驱动程序提供动力。
|
||||
- 使用 Appium 驱动程序 CLI 与他人分享驱动程序很容易。 没有中央权威。
|
||||
任何人都可以公开或私下分享驱动程序,免费或收费。 驱动程序可以是开源或闭源的(尽管我们欣赏开源!)。
|
||||
|
||||
Appium's vision of being a platform for development extends beyond the support of automation for
|
||||
all app platforms. As a popular automation tool, there are many opportunities for integrating
|
||||
Appium with all kinds of other tools and services. In addition, there are many feature ideas for
|
||||
Appium, either as a core server or in its incarnation across various drivers, which the core team
|
||||
will never have time to build. And so, with Appium 2, Appium has released a plugin system that
|
||||
enables anyone to build and share modules that change how Appium works!
|
||||
Appium 作为开发平台的愿景超越了对所有应用平台自动化支持。 作为一个流行的自动化工具,有许多机会将 Appium 与各种其他工具和服务集成。 此外,Appium 有许多功能想法,要么作为核心服务器,要么在其各种驱动程序中的化身,核心团队没有足够的时间构建。 因此,Appium 2 发布了一个插件系统,使任何人都可以构建和分享改变 Appium 工作方式的模块!
|
||||
|
||||
In the same way that drivers are easily shareable and consumable via the Appium driver CLI, plugins
|
||||
can be published and consumed via a parallel [Plugin CLI](../reference/cli/extensions.md). Plugins
|
||||
can do all sorts of things, for example adding the ability for Appium to find and interact with
|
||||
screen regions based on a template image (as in the images). There are very few
|
||||
limitations on what you can do with plugins, so you might also be interested in learning how to
|
||||
[Build Plugins](../developing/build-plugins.md) in Node.js that can be used with Appium.
|
||||
就像驱动程序可以通过 Appium 驱动程序 CLI 轻松分享和消费一样,插件可以通过[插件 CLI](../reference/cli/extensions.md)发布和消费。 插件可以做各种事情,例如添加 Appium 根据模板图像查找和与屏幕区域交互的能力(例如[图像插件](https://github.com/appium/appium/tree/master/packages/images-plugin))。 插件几乎没有限制,所以您可能也对学习如何在 Node.js 中[构建插件](../developing/build-plugins.md)以与 Appium 一起使用感兴趣。
|
||||
|
||||
So that's Appium: an extensible, universal interface for the UI automation of potentially
|
||||
everything! Read on into some of the specific intro docs for more details, or check out the various
|
||||
guides to dive into some more general concepts and features of Appium.
|
||||
所以这就是 Appium:一个可扩展的、潜在一切的 UI 自动化通用接口! 继续阅读一些具体的介绍文档以获取更多细节,或查看各种指南以深入了解 Appium 的一些更一般概念和功能。
|
||||
|
||||
@@ -2,57 +2,31 @@
|
||||
hide:
|
||||
- toc
|
||||
|
||||
title: Intro to Appium Clients
|
||||
title: Appium 客户端简介
|
||||
---
|
||||
|
||||
For all the reasons discussed in the [main Overview](./appium.md), Appium is based on the W3C
|
||||
WebDriver specification. This means that
|
||||
Appium implements a client-server architecture. The server (consisting of Appium itself along with
|
||||
any drivers or plugins you are using for automation) is connected to the devices under test, and
|
||||
is actually responsible for making automation happen on those devices. The client (driven by _you_,
|
||||
the Appium test author) is responsible for sending commands to the server over the network, and
|
||||
receiving responses from the server as a result. These responses can be used to tell whether
|
||||
automation commands are successful, or might contain information that you queried about the state
|
||||
of the application. This document is a conceptual introduction to the client side of this equation.
|
||||
出于[概述](./appium.md)中讨论的所有原因,Appium 基于 [W3C WebDriver 规范](https://w3c.github.io/webdriver/)。 这意味着 Appium 实现了客户端-服务器架构。 服务器(由 Appium 本身以及您用于自动化的任何驱动程序或插件组成)连接到测试设备,并实际负责在这些设备上实现自动化。 客户端(由_您_驱动,Appium 测试作者)负责通过网络向服务器发送命令,并接收来自服务器的响应。 这些响应可用于判断自动化命令是否成功,或可能包含您查询的关于应用程序状态的信息。 本文档是对客户端方面的概念性介绍。
|
||||
|
||||
!!! info
|
||||
|
||||
```
|
||||
For more about the server side of the equation (i.e., how does Appium actually control
|
||||
devices?), check out our [Intro to Appium Drivers](./drivers.md). To skip to a list of links to
|
||||
Appium client libraries, check out the [list of clients](../ecosystem/clients.md).
|
||||
有关服务器端的信息(即 Appium 如何实际控制设备?),请查看我们的 [Appium 驱动程序简介](./drivers.md)。要跳转到 Appium 客户端库链接列表,请查看[客户端列表](../ecosystem/clients.md)。
|
||||
```
|
||||
|
||||
What sorts of automation commands are available? That is up to the particular driver and plugins
|
||||
that you are using in any given session. A standard set of commands would include, for example, the
|
||||
following:
|
||||
有哪些自动化命令可用? 那取决于您在任何给定会话中使用的特定驱动程序和插件。 一组标准命令可能包括以下内容:
|
||||
|
||||
- Find Element
|
||||
- Click Element
|
||||
- Get Page Source
|
||||
- Take Screenshot
|
||||
- 查找元素
|
||||
- 点击元素
|
||||
- 获取页面源代码
|
||||
- 截取屏幕截图
|
||||
|
||||
If you look at these commands in the WebDriver specification, you'll notice that they are not
|
||||
defined in terms of any particular programming language. They are not Java commands, or JavaScript
|
||||
commands, or Python commands. Instead, they form part of an HTTP API which can be accessed from
|
||||
within _any_ programming language (or none! you could just use cURL if you want).
|
||||
如果您查看 WebDriver 规范中的这些命令,您会注意到它们不是以任何特定编程语言定义的。 它们不是 Java 命令、JavaScript 命令或 Python 命令。 相反,它们形成了可以通过_任何_编程语言访问的 HTTP API( 如果您愿意,您可以只使用 cURL)。
|
||||
|
||||
So, for example, the `Find Element` command corresponds to an HTTP `POST` request sent to the HTTP
|
||||
endpoint `/session/:sessionid/element` (where in this case, `:sessionid` is a placeholder for the
|
||||
unique session ID generated by the server in a previous call to `Create Session`).
|
||||
例如,`查找元素`命令对应于发送到 HTTP 端点 `/session/:sessionid/element` 的 HTTP `POST` 请求(其中`:sessionid` 是服务器在之前 `创建会话` 调用中生成的唯一会话 ID 的占位符)。
|
||||
|
||||
This information is primarily useful for people developing technology that works with the WebDriver
|
||||
spec. It's not particularly useful for people trying to write Appium or Selenium tests. When you
|
||||
write an Appium test, you want to use a programming language you're familiar with. Luckily, there
|
||||
exist a set of [Appium client libraries](../ecosystem/clients.md)[^1] that take care of the
|
||||
responsibility of speaking HTTP to the Appium server. Instead, they expose a set of "native"
|
||||
commands for a particular programming language, so that, to the test author, it just feels like
|
||||
you're writing Python, or JavaScript, or Java.
|
||||
此信息主要对开发与 WebDriver 规范一起工作的技术的人有用。 它对试图编写 Appium 或 Selenium 测试的人不是特别有用。 当您编写 Appium 测试时,您希望使用您熟悉的编程语言。 幸运的是,存在一组 [Appium 客户端库](../ecosystem/clients.md)[^1],它们负责向 Appium 服务器发起HTTP 的请求。 它们为特定编程语言公开了一组“原生”命令,以便对测试作者来说,它就像您在编写 Python、JavaScript 或 Java。
|
||||
|
||||
As an example, here's the same simple set of Appium commands in five different programming
|
||||
languages, using the recommended Appium client binding for each language (note that this is not
|
||||
working sample code including all appropriate imports; see each client library's instructions for
|
||||
setup and command reference):
|
||||
以下是使用 Appium 客户端在五种不同编程语言中实现相同的 Appium 命令集的示例(注意,这不是包含所有导入的工作示例代码;请参阅每个客户端库的设置和命令参考说明):
|
||||
|
||||
\=== "JavaScript (Webdriver.io)"
|
||||
|
||||
@@ -109,31 +83,18 @@ System.Console.WriteLine(driver.PageSource);
|
||||
```
|
||||
````
|
||||
|
||||
Each of these scripts, despite being in different languages, does the same thing under the hood:
|
||||
尽管这些脚本使用不同的编程语言,但它们在底层做的事情是相同的:
|
||||
|
||||
1. Call `Find Element` with a `using` parameter of `xpath` and a `value` parameter expressing the
|
||||
XPath query used to find an element. (If you're confused about these terms, you might find an
|
||||
introduction to Appium or Selenium useful)
|
||||
2. Call `Click Element` with the ID of the element found in the previous call.
|
||||
3. Call `Get Element Text` with the ID of the same element, and print it to the console.
|
||||
4. Call `Get Page Source` to retrieve the page/app source and print it to the console.
|
||||
1. 调用 `Find Element`,使用 `using` 参数值为 `xpath`,以及 `value` 参数表达用于查找元素的 XPath 查询。 (如果你对这些术语感到困惑,可以参考 Appium 或 Selenium 的入门介绍)
|
||||
2. 使用在上一次调用中找到的元素 ID 调用 `Click Element`。
|
||||
3. 使用同一元素的 ID 调用 `Get Element Text`,并将其打印到控制台。
|
||||
4. 调用 `Get Page Source` 来检索页面/应用程序源代码并将其打印到控制台。
|
||||
|
||||
The only other thing to keep in mind before choosing or using a client is that each client is
|
||||
independently maintained. Just because a feature is available in one client, it doesn't mean it's
|
||||
available in another client (though all clients support at least the standard W3C protocol plus any
|
||||
common appium extensions). Just because one client has a nice set of helper functions, doesn't mean
|
||||
another will. Some clients are kept very frequently up to date, and others are not! So when
|
||||
thinking about choosing a library, the first consideration is the language you want to use, and the
|
||||
second consideration is how full-featured and well-maintained the library is!
|
||||
在选择或使用客户端之前,需要记住的另一件事是每个客户端都是独立维护的。 仅仅因为某个功能在一个客户端中可用,并不意味着它在另一个客户端中也可用(尽管所有客户端至少支持标准的 W3C 协议以及任何常见的 Appium 扩展)。 仅仅因为一个客户端有一套很好的辅助函数,并不意味着另一个客户端也会有。 有些客户端更新非常频繁,而其他客户端则不然! 因此,在考虑选择一个库时,首要考虑的是你想使用的语言,第二个考虑因素是该库的功能完善程度和维护情况!
|
||||
|
||||
To learn how to use an Appium client, visit that client's homepage to learn more. In many cases,
|
||||
the Appium client for a given language is built _on top of_ the _Selenium_ client for that
|
||||
language, and so certain Appium clients may only document the features which the Appium client
|
||||
added on top of the Selenium client. All that to say, for a full reference, you may need to visit
|
||||
both the Appium client documentation as well as the Selenium client documentation.
|
||||
要学习如何使用 Appium 客户端,请访问该客户端的主页以了解更多信息。 在许多情况下,特定语言的 Appium 客户端是在该语言的 _Selenium_ 客户端之上构建的,因此某些 Appium 客户端可能只记录 Appium 客户端在 Selenium 客户端基础上添加的功能。 也就是说,要获得完整的参考,你可能需要访问 Appium 客户端文档以及 Selenium 客户端文档。
|
||||
|
||||
That's all you need to know about Appium clients! Head over to the
|
||||
[Clients](../ecosystem/clients.md) page to check out the current list of clients.
|
||||
这就是你需要了解的关于 Appium 客户端的全部内容! 前往 [客户端](../ecosystem/clients.md) 页面查看当前的客户端列表。
|
||||
|
||||
[^1]: These libraries are alternately called "clients", "client libraries", or "client bindings".
|
||||
They all mean the same thing!
|
||||
[^1]: 这些库也被称为"客户端"、"客户端库"或"客户端绑定"。
|
||||
它们的意思都是一样的!
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -1,81 +1,64 @@
|
||||
---
|
||||
title: Install the UiAutomator2 Driver
|
||||
title: 安装 UiAutomator2 驱动程序
|
||||
---
|
||||
|
||||
You can't do much with Appium unless you have a [driver](../intro/drivers.md), which is an
|
||||
interface that allows Appium to automate a particular platform.
|
||||
除非您有[驱动程序](../intro/drivers.md),否则您无法使用 Appium 做太多事情,驱动程序是一个允许 Appium 自动化特定平台的接口。
|
||||
|
||||
!!! info
|
||||
|
||||
```
|
||||
For this quickstart guide, we're going to be automating an app on the Android platform, because
|
||||
the system requirements for Android automation via Appium are the same as for Appium itself
|
||||
(whereas the iOS driver, for example, requires you to be using macOS).
|
||||
对于这个快速入门指南,我们将在 Android 平台上自动化一个应用,因为通过 Appium 进行 Android 自动化的系统要求与 Appium 本身相同(而 iOS 驱动程序,则要求您使用 macOS)。
|
||||
```
|
||||
|
||||
The driver we're going to use is called the UiAutomator2
|
||||
Driver. It's worth visiting that driver's
|
||||
documentation and bookmarking it, because it will be an invaluable reference down the line.
|
||||
我们要使用的驱动程序称为 [UiAutomator2 驱动程序](https://github.com/appium/appium-uiautomator2-driver)。 请访问该驱动程序的文档并将其加入书签,因为它将成为未来宝贵的参考。
|
||||
|
||||
## Set up Android automation requirements
|
||||
## 设置 Android 自动化要求
|
||||
|
||||
According to the driver, in addition to a working Appium server, we also need to set up the following:
|
||||
根据驱动程序,除了工作 Appium 服务器,我们还需要设置以下内容:
|
||||
|
||||
### Android SDK
|
||||
|
||||
- The easiest way to set up the Android SDK requirements is by downloading [Android Studio](https://developer.android.com/studio).
|
||||
We need to use its SDK manager (_Settings -> Languages & Frameworks -> Android SDK_)
|
||||
to download the following items:
|
||||
- Android SDK Platform (select whichever Android platform we want to automate, for example, API level 30)
|
||||
- Android SDK Platform-Tools
|
||||
- If you wish, you can also download these items without Android Studio:
|
||||
- Android SDK Platform can be downloaded using `sdkmanager` included in [Android command-line tools](https://developer.android.com/studio#command-line-tools-only)
|
||||
- [Android SDK Platform-Tools](https://developer.android.com/tools/releases/platform-tools)
|
||||
- Set up the `ANDROID_HOME` environment variable to point to the directory where the Android SDK is
|
||||
installed. You can usually find the path to this directory in the Android Studio SDK manager. It
|
||||
will contain the `platform-tools` and other directories.
|
||||
- 设置 Android SDK 的最简单方法是下载 [Android Studio](https://developer.android.com/studio)。
|
||||
我们需要使用其 SDK 管理器(_设置 -> 语言和框架 -> Android SDK_)下载以下项目:
|
||||
- Android SDK 平台(选择我们想要自动化的任何 Android 平台,例如 API 级别 30)
|
||||
- Android SDK 平台工具
|
||||
- 如果您愿意,您也可以在没有 Android Studio 的情况下下载这些项目:
|
||||
- Android SDK 平台可以使用包含在 [Android 命令行工具](https://developer.android.com/studio#command-line-tools-only) 中的 `sdkmanager` 下载
|
||||
- [Android SDK 平台工具](https://developer.android.com/tools/releases/platform-tools)
|
||||
- 设置 `ANDROID_HOME` 环境变量指向安装 Android SDK 的目录。 您通常可以在 Android Studio SDK 管理器中找到此目录的路径。 它将包含 `platform-tools` 和其他目录。
|
||||
|
||||
### Java JDK
|
||||
|
||||
- Install the Java JDK (for the most recent Android API levels, JDK 9 is required, otherwise JDK
|
||||
8 is required). You can download this from [Oracle](https://jdk.java.net/) or [Adoptium](https://adoptium.net/en-GB/temurin/releases/).
|
||||
Make sure you get the JDK and not the JRE.
|
||||
- Set up the `JAVA_HOME` environment variable to point to the JDK home directory. It will contain
|
||||
the `bin`, `include`, and other directories.
|
||||
- 安装 Java JDK(对于最新的 Android API 级别,需要 JDK 9,否则请用 JDK 8)。 您可以从 [Oracle](https://jdk.java.net/) 或 [Adoptium](https://adoptium.net/en-GB/temurin/releases/) 下载此软件。
|
||||
确保您获得 JDK 而不是 JRE。
|
||||
- 设置 `JAVA_HOME` 环境变量指向 JDK 主目录。 它将包含 `bin`、`include` 和其他目录。
|
||||
|
||||
### Prepare the Device
|
||||
### 准备设备
|
||||
|
||||
- If using an emulator, use Android Studio to create and launch an Android Virtual Device (AVD).
|
||||
You may need to download the system images for the API level of the emulator you want to
|
||||
create. Using the AVD creation wizard in Android Studio is generally the easiest way to do all of
|
||||
this.
|
||||
- If using a real device, you should [set it up for development and enable USB Debugging](https://developer.android.com/studio/debug/dev-options).
|
||||
- With the emulator or device connected, you can run `adb devices` (via the binary located at
|
||||
`$ANDROID_HOME/platform-tools/adb`) to verify that your device shows up as connected.
|
||||
- 如果使用模拟器,使用 Android Studio 创建和启动 Android 虚拟设备 (AVD)。
|
||||
您可能需要下载要创建的模拟器的 API 级别的系统映像。 使用 Android Studio 中的 AVD 创建向导通常是完成所有这些的最简单方法。
|
||||
- 如果使用真实设备,您应该为其设置[开发者模式并启用 USB 调试](https://developer.android.com/studio/debug/dev-options)。
|
||||
- 连接模拟器或设备后,您可以运行 `adb devices`(adb位于 `$ANDROID_HOME/platform-tools/adb`)来验证您的设备是否显示为已连接。
|
||||
|
||||
Once your device shows up as connected in `adb`, and you've verified that the environment variables
|
||||
are set up correctly, you should be good to go! If you ran into problems with any of these steps,
|
||||
refer to the driver documentation, or the various Android or Java documentation sites as necessary.
|
||||
一旦您的设备在 `adb` 中显示为已连接,并且您验证了环境变量设置正确,您应该准备好了! 如果您在这些步骤中的任何一个遇到问题,请参考驱动程序文档,或根据需要参考各种 Android 或 Java 文档站点。
|
||||
|
||||
Also, congratulations: whether or not you intended to, you now have the Android developer toolchain
|
||||
set up on your system, so you can get busy making Android apps if you want!
|
||||
恭喜:无论您是否打算这样做,您现在已经在系统上设置了 Android 开发者工具链,所以如果您愿意,可以开始制作 Android 应用!
|
||||
|
||||
## Install the driver itself
|
||||
## 安装驱动程序本身
|
||||
|
||||
### Standard Install
|
||||
### 标准安装
|
||||
|
||||
Like all Appium drivers, the UiAutomator2 driver is installed via the [Appium Extension CLI](../reference/cli/extensions.md).
|
||||
Since UiAutomator2 is maintained by the core Appium team, it has an 'official' driver name
|
||||
(`uiautomator2`), which makes the installation simpler.
|
||||
像所有 Appium 驱动程序一样,UiAutomator 2 驱动程序通过 [Appium 扩展 CLI](../reference/cli/extensions.md) 安装。
|
||||
由于 UiAutomator 2 由核心 Appium 团队维护,它有一个“官方”驱动程序名称(`uiautomator2`),这使得安装更简单。
|
||||
|
||||
Before installing, make sure your Appium server is _not_ running (if it is, quit it with _Ctrl-C_).
|
||||
Then run the following command:
|
||||
在安装之前,确保您的 Appium 服务器_未_运行(如果正在运行,请用 _Ctrl-C_ 退出)。
|
||||
然后运行以下命令:
|
||||
|
||||
```bash
|
||||
appium driver install uiautomator2
|
||||
```
|
||||
|
||||
It should produce output that looks something like:
|
||||
它应该产生看起来像这样的输出:
|
||||
|
||||
```
|
||||
Attempting to find and install driver 'uiautomator2'
|
||||
@@ -85,57 +68,45 @@ Driver uiautomator2@2.0.5 successfully installed
|
||||
- platformNames: ["Android"]
|
||||
```
|
||||
|
||||
Note how the installation process specifies what platforms is the driver valid for (in this case,
|
||||
`Android`), and what automation name (the `appium:automationName` [capability](../guides/caps.md))
|
||||
must be used to select this driver for use during an Appium session (in this case, `UiAutomator2`).
|
||||
注意安装过程如何指定驱动程序的适用平台(在这里选 `Android`),以及必须用于在 Appium 会话期间选择此驱动程序的自动化名称(`appium:automationName` [能力](../guides/caps.md))(在这里选 `UiAutomator2`)。
|
||||
|
||||
!!! note
|
||||
|
||||
```
|
||||
In this quickstart we have used the [Extension CLI](../reference/cli/extensions.md) to install the
|
||||
UiAutomator2 driver, but if you are incorporating Appium into a Node.js project, you might
|
||||
prefer to use `npm` to manage Appium and its connected drivers. To learn more about this
|
||||
technique, visit the guide on [managing Appium extensions](../guides/managing-exts.md).
|
||||
在这个快速入门中,我们使用了 [CLI扩展](../reference/cli/extensions.md) 来安装 UiAutomator 2 驱动程序,但如果您将 Appium 纳入 Node.js 项目,您可能更喜欢使用 `npm` 来管理 Appium 及其连接的驱动程序。要了解更多关于此技术的信息,请访问关于[管理 Appium 扩展](../guides/managing-exts.md)的指南。
|
||||
```
|
||||
|
||||
### Batch Install
|
||||
### 批量安装
|
||||
|
||||
You may want to use Appium with more than one driver. One way to accomplish this is to run
|
||||
`appium driver install <driver-name>` for each individual driver, but you can also install multiple
|
||||
drivers in one go:
|
||||
您可能希望将 Appium 与多个驱动程序一起使用。 一种方法是为每个单独的驱动程序运行 `appium driver install <driver-name>`,但您也可以一次性安装多个驱动程序:
|
||||
|
||||
```
|
||||
appium setup
|
||||
```
|
||||
|
||||
Running this will install Appium's mobile-specific drivers: UiAutomator2, [XCUITest](https://appium.github.io/appium-xcuitest-driver/)
|
||||
(only if running macOS), and [Espresso](https://github.com/appium/appium-espresso-driver).
|
||||
运行此命令将安装 Appium 的移动端特定驱动程序:UiAutomator2、[XCUITest](https://appium.github.io/appium-xcuitest-driver/)(仅在运行 macOS 时),以及 [Espresso](https://github.com/appium/appium-espresso-driver)。
|
||||
|
||||
You can also use this command to batch install drivers for desktop applications or desktop browsers.
|
||||
For more details on this, refer to the [Setup command documentation](../reference/cli/setup.md).
|
||||
您也可以使用此命令批量安装桌面应用程序或桌面浏览器的驱动程序。
|
||||
有关更多详细信息,请参考[设置命令文档](../reference/cli/setup.md)。
|
||||
|
||||
### Validating the Install
|
||||
### 验证安装
|
||||
|
||||
The UiAutomator2 driver, like all official Appium drivers, comes with the Appium Doctor tool, which
|
||||
allows validating whether all prerequisites have been set up correctly:
|
||||
UiAutomator2 驱动程序,像所有官方 Appium 驱动程序一样,带有 Appium Doctor 工具,它允许验证是否所有先决条件都已正确设置:
|
||||
|
||||
```
|
||||
appium driver doctor uiautomator2
|
||||
```
|
||||
|
||||
This guide has focused on essential requirements, so Appium Doctor may suggest one or more optional
|
||||
fixes. But if you see `0 required fixes needed`, that means everything is set up!
|
||||
本指南专注于基本要求,因此 Appium Doctor 可能建议一个或多个可选修复。 但如果您看到 `0 required fixes needed`,那意味着一切都设置好了!
|
||||
|
||||
Now, start the Appium server again (run `appium`), and you should see that the newly-installed
|
||||
driver is listed as available:
|
||||
现在,再次启动 Appium 服务器(运行 `appium`),您应该看到新安装的驱动程序列为可用:
|
||||
|
||||
```
|
||||
[Appium] Available drivers:
|
||||
[Appium] - uiautomator2@2.0.5 (automationName 'UiAutomator2')
|
||||
```
|
||||
|
||||
With the Android setup complete and the UiAutomator2 driver installed, you're ready to write your
|
||||
first test! Now select your preferred language and give it a shot:
|
||||
Android 设置完成并安装了 UiAutomator2 驱动程序,准备编写第一个测试吧! 现在选择您喜欢的语言并试试:
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import betterAjvErrors from '@sidvind/better-ajv-errors';
|
||||
import {lilconfig} from 'lilconfig';
|
||||
import _ from 'lodash';
|
||||
import yaml from 'yaml';
|
||||
import * as yaml from 'yaml';
|
||||
import {getSchema, validate} from './schema/schema';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,8 +5,8 @@
|
||||
import B from 'bluebird';
|
||||
import {env, fs} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import YAML from 'yaml';
|
||||
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';
|
||||
import {packageDidChange} from './package-changed';
|
||||
|
||||
@@ -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.1",
|
||||
"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,35 +61,32 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.1.1",
|
||||
"@appium/base-plugin": "^3.0.4",
|
||||
"@appium/docutils": "^2.1.2",
|
||||
"@appium/logger": "^2.0.2",
|
||||
"@appium/schema": "^1.0.0",
|
||||
"@appium/support": "^7.0.3",
|
||||
"@appium/types": "^1.1.1",
|
||||
"@sidvind/better-ajv-errors": "4.0.0",
|
||||
"@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.2",
|
||||
"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.2",
|
||||
"type-fest": "5.2.0",
|
||||
"winston": "3.18.3",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "2.8.1"
|
||||
"teen_process": "4.0.8",
|
||||
"type-fest": "5.4.1",
|
||||
"winston": "3.19.0",
|
||||
"ws": "8.19.0",
|
||||
"yaml": "2.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "9.20.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';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {env, fs, npm, tempDir} from '@appium/support';
|
||||
import path from 'node:path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import YAML from 'yaml';
|
||||
import * as YAML from 'yaml';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_INSTALL as INSTALL,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {fs, tempDir} from '@appium/support';
|
||||
import path from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
import * as YAML from 'yaml';
|
||||
import {
|
||||
CACHE_DIR_RELATIVE_PATH,
|
||||
CURRENT_SCHEMA_REV,
|
||||
|
||||
@@ -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,9 +1,8 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'fs';
|
||||
// eslint-disable-next-line import/named
|
||||
import fs from 'node:fs';
|
||||
import {createSandbox} from 'sinon';
|
||||
import YAML from 'yaml';
|
||||
import * as YAML from 'yaml';
|
||||
import * as schema from '../../lib/schema/schema';
|
||||
import {resolveFixture, rewiremock} from '../helpers';
|
||||
|
||||
|
||||
@@ -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,28 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [10.1.1](https://github.com/appium/appium/compare/@appium/base-driver@10.1.0...@appium/base-driver@10.1.1) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
@@ -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.1",
|
||||
"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.3",
|
||||
"@appium/types": "^1.1.1",
|
||||
"@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.0",
|
||||
"express": "5.1.0",
|
||||
"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.2",
|
||||
"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.2.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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user