mirror of
https://github.com/appium/appium.git
synced 2026-11-06 17:55:21 +08:00
Compare commits
240
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b67de03966 | ||
|
|
2b39493e98 | ||
|
|
e916673e32 | ||
|
|
76ff5aa69a | ||
|
|
b1b313b989 | ||
|
|
658f88d8c1 | ||
|
|
3999208fc0 | ||
|
|
1f945716a0 | ||
|
|
0a3ad48419 | ||
|
|
17be28c750 | ||
|
|
746073148f | ||
|
|
73ef2f7d43 | ||
|
|
6fb07a5dbc | ||
|
|
5dd80daf63 | ||
|
|
05e36962e6 | ||
|
|
eaa824f601 | ||
|
|
6cb62d21e8 | ||
|
|
8254a7a3b5 | ||
|
|
83e99bc752 | ||
|
|
147c0bcac3 | ||
|
|
ddd4251108 | ||
|
|
b19bd7878b | ||
|
|
91446df648 | ||
|
|
d5b316031e | ||
|
|
737cf77b6e | ||
|
|
c35a12a51d | ||
|
|
5c2aafa208 | ||
|
|
4422d99d45 | ||
|
|
ce50dd7fa7 | ||
|
|
f06226b7bb | ||
|
|
33459705ed | ||
|
|
f4ca259852 | ||
|
|
a49ae68f13 | ||
|
|
50bd67e59d | ||
|
|
549b4d4179 | ||
|
|
26ffad4f24 | ||
|
|
1a81ac88f3 | ||
|
|
c9fca97cae | ||
|
|
7d87b26047 | ||
|
|
87930a2df1 | ||
|
|
29ee04cf03 | ||
|
|
8892b1022d | ||
|
|
b97fe497cf | ||
|
|
a729d86637 | ||
|
|
e5bd73272b | ||
|
|
037b42454a | ||
|
|
5cf259f974 | ||
|
|
1486078c82 | ||
|
|
01a4eaec49 | ||
|
|
10086b543f | ||
|
|
68ce0e2bdd | ||
|
|
ae4ca98826 | ||
|
|
1d1631e64e | ||
|
|
5721d50b3a | ||
|
|
bd3a120587 | ||
|
|
3e2592804a | ||
|
|
93c0842dbb | ||
|
|
13e8834405 | ||
|
|
3ab2320e1b | ||
|
|
258fc2245a | ||
|
|
5dd9baf5ea | ||
|
|
481807f8db | ||
|
|
8a90e74af3 | ||
|
|
ea9595ff53 | ||
|
|
5d796139f6 | ||
|
|
b3f8a5eea6 | ||
|
|
ae549c8613 | ||
|
|
af8efc0882 | ||
|
|
63103cfa0d | ||
|
|
b0938e6334 | ||
|
|
4b0cc9d37f | ||
|
|
909cc3d4c0 | ||
|
|
1a8f88029b | ||
|
|
3a8049887d | ||
|
|
2ed7bb04d8 | ||
|
|
db87f3cdd4 | ||
|
|
2c0f3fd9e3 | ||
|
|
63b167c408 | ||
|
|
406dfad41f | ||
|
|
85574e7e80 | ||
|
|
336395fcfa | ||
|
|
d9e8b3ba73 | ||
|
|
e625408f55 | ||
|
|
d2a981a45f | ||
|
|
76f2dd3b36 | ||
|
|
03dac9d6e8 | ||
|
|
eb93d49802 | ||
|
|
3f21bcf1a7 | ||
|
|
d708029325 | ||
|
|
485b8d7a70 | ||
|
|
c688932525 | ||
|
|
77d159838f | ||
|
|
c733c090f6 | ||
|
|
2a07c2818c | ||
|
|
52cbfd2708 | ||
|
|
a551c38171 | ||
|
|
0f42831afa | ||
|
|
5c870329a3 | ||
|
|
a236fdbc97 | ||
|
|
a2e9cef145 | ||
|
|
7d72502769 | ||
|
|
12a91e5293 | ||
|
|
d093b76bef | ||
|
|
120b9ddfac | ||
|
|
281e61c297 | ||
|
|
8ac6bfcc3b | ||
|
|
cdd448800e | ||
|
|
e977a29b0b | ||
|
|
2d8fa2662e | ||
|
|
b96c8fa718 | ||
|
|
c6a88e7dd7 | ||
|
|
087a83a27e | ||
|
|
6a241d760a | ||
|
|
e648569dfb | ||
|
|
ea6a2e117f | ||
|
|
4fffe28f89 | ||
|
|
a4a9022303 | ||
|
|
8fd47be066 | ||
|
|
0af2a4bfd8 | ||
|
|
e0c9018453 | ||
|
|
f4d14c1969 | ||
|
|
e44e5ea918 | ||
|
|
32f0bdf870 | ||
|
|
bf30246a65 | ||
|
|
764cfbae75 | ||
|
|
5f0534f154 | ||
|
|
0b7e7419ef | ||
|
|
fee213e49d | ||
|
|
2fa12e915a | ||
|
|
f943fab141 | ||
|
|
6609b2be57 | ||
|
|
f17899a0f5 | ||
|
|
350eab89a5 | ||
|
|
57092711c0 | ||
|
|
cac18e30b5 | ||
|
|
77d249226a | ||
|
|
154ea18252 | ||
|
|
0cfafb23c0 | ||
|
|
b56646198c | ||
|
|
13d3b615e6 | ||
|
|
7c0d771408 | ||
|
|
0a40623085 | ||
|
|
b9ad1f1712 | ||
|
|
608668ed47 | ||
|
|
73d4b7760b | ||
|
|
5112541a34 | ||
|
|
cfde82a40e | ||
|
|
0f3fd55c65 | ||
|
|
67cf081098 | ||
|
|
d63f5441c7 | ||
|
|
0bb61073e2 | ||
|
|
52eb9e308e | ||
|
|
868153fee4 | ||
|
|
c25293b23e | ||
|
|
4c380d7604 | ||
|
|
1e93aec531 | ||
|
|
22f8a9b04d | ||
|
|
1a7199fbbc | ||
|
|
fa9c4b2aab | ||
|
|
3836f0e614 | ||
|
|
6e3fd0a8bc | ||
|
|
d5b1393285 | ||
|
|
123b0e1908 | ||
|
|
eabe372b0b | ||
|
|
416c3349b7 | ||
|
|
ad720f5ca0 | ||
|
|
6d31d6acb2 | ||
|
|
4e93a63ac5 | ||
|
|
7233dd315a | ||
|
|
97d53ffa5b | ||
|
|
2cec9fa27f | ||
|
|
a1a7b7e981 | ||
|
|
228103f489 | ||
|
|
9dcb2c4056 | ||
|
|
a9dd7f5e41 | ||
|
|
de2cd50b96 | ||
|
|
ccb3106814 | ||
|
|
f707dfa174 | ||
|
|
53b2db196d | ||
|
|
279ebb5cd6 | ||
|
|
f1fc5a6869 | ||
|
|
24586d4ed9 | ||
|
|
4915433aae | ||
|
|
fd70da7ab4 | ||
|
|
fa769cebd9 | ||
|
|
1eb55e85d7 | ||
|
|
f98eea77d1 | ||
|
|
4cbcf95d77 | ||
|
|
4c947872f1 | ||
|
|
11882be379 | ||
|
|
e3334b4dd7 | ||
|
|
a10f403bb1 | ||
|
|
08d50889c2 | ||
|
|
d3628a9410 | ||
|
|
8d6b5f87c7 | ||
|
|
531b20a3c1 | ||
|
|
6699d1b266 | ||
|
|
37a6006537 | ||
|
|
1b449709d1 | ||
|
|
0e4ecc49d6 | ||
|
|
bf29503054 | ||
|
|
ab0633a70c | ||
|
|
0c44ddeebe | ||
|
|
d9eb36f413 | ||
|
|
b483d82b13 | ||
|
|
93e81777b9 | ||
|
|
623ef38f2a | ||
|
|
5c696c5c91 | ||
|
|
98215066ee | ||
|
|
8026a953aa | ||
|
|
c8556f172e | ||
|
|
e9a41bd9ae | ||
|
|
e810f2f23d | ||
|
|
c04c38d1e6 | ||
|
|
d9c4e4c4ad | ||
|
|
bce22834ba | ||
|
|
ac65be5a1e | ||
|
|
1e570c4d29 | ||
|
|
52ec456d17 | ||
|
|
a89e429a84 | ||
|
|
4b0666d7d5 | ||
|
|
816ca05020 | ||
|
|
31036f65b2 | ||
|
|
4bb987c037 | ||
|
|
0162f3ab4e | ||
|
|
813c6b5b7a | ||
|
|
edc2013073 | ||
|
|
979a6e4c9d | ||
|
|
1cc4f0f2e5 | ||
|
|
0467311286 | ||
|
|
cb9c476fbb | ||
|
|
44278bdfc3 | ||
|
|
a8418f95e9 | ||
|
|
7e67b54726 | ||
|
|
e6b583a209 | ||
|
|
3016201f2a | ||
|
|
e0d5c4dfd6 | ||
|
|
779a3a048f | ||
|
|
07302ab3e9 | ||
|
|
609db021f4 |
@@ -0,0 +1,10 @@
|
||||
<!-- CODEGRAPH_START -->
|
||||
## CodeGraph
|
||||
|
||||
In repositories indexed by CodeGraph (a `.codegraph/` directory exists at the repo root), reach for it BEFORE grep/find or reading files when you need to understand or locate code:
|
||||
|
||||
- **MCP tool** (when available): `codegraph_explore` answers most code questions in one call — the relevant symbols' verbatim source plus the call paths between them, including dynamic-dispatch hops grep can't follow. Name a file or symbol in the query to read its current line-numbered source. If it's listed but deferred, load it by name via tool search.
|
||||
- **Shell** (always works): `codegraph explore "<symbol names or question>"` prints the same output.
|
||||
|
||||
If there is no `.codegraph/` directory, skip CodeGraph entirely — indexing is the user's decision.
|
||||
<!-- CODEGRAPH_END -->
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["mcp__codegraph__*"]
|
||||
},
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "codegraph prompt-hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codegraph": {
|
||||
"type": "stdio",
|
||||
"command": "codegraph",
|
||||
"args": ["serve", "--mcp", "--path", "."]
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,13 +1,14 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
[*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,json,jsonc}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
@@ -3,7 +3,6 @@ description: Create a new bug report.
|
||||
title: '🐞 bug: <title>'
|
||||
labels: [Bug, Needs Triage]
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: ':stop_sign: _Make sure you are reporting an issue. For general questions, please use [http://discuss.appium.io](http://discuss.appium.io)._'
|
||||
@@ -13,9 +12,9 @@ body:
|
||||
label: Is there an existing issue for this?
|
||||
description: 'Please [search :mag: the issues](https://github.com/appium/appium/issues) to check if this bug has already been reported.'
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Appium Version
|
||||
@@ -49,7 +48,7 @@ body:
|
||||
description: |
|
||||
You can retrieve the latest server/driver/plugin version number by running `npm info <package-name> version`. If you are not up to date, please try to reproduce your issue after updating all affected components.
|
||||
options:
|
||||
- label: I confirm the issue is still reproducible with the latest component versions
|
||||
- label: I confirm the issue is still reproducible with the latest component versions
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -82,10 +81,10 @@ body:
|
||||
|
||||
:warning: _Remember to redact or remove any sensitive information!_
|
||||
value: |
|
||||
- Debug details (`appium --show-debug-info`):
|
||||
- Last component version(s) which did _not_ exhibit the problem:
|
||||
- Platform and version of device under test:
|
||||
- Real device or emulator/simulator:
|
||||
- Debug details (`appium --show-debug-info`):
|
||||
- Last component version(s) which did _not_ exhibit the problem:
|
||||
- Platform and version of device under test:
|
||||
- Real device or emulator/simulator:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: ❓ Question
|
||||
- name: ❓ Question
|
||||
url: https://discuss.appium.io/
|
||||
about: Please use the Appium forum for any questions.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project.
|
||||
title: "💡 feat: <title>"
|
||||
title: '💡 feat: <title>'
|
||||
labels: [Enhancement]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Use Node.js ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: npm
|
||||
@@ -33,8 +33,44 @@ runs:
|
||||
run: |
|
||||
# Replace npm with sfw npm in the install command
|
||||
install_cmd="${{ inputs.install-command }}"
|
||||
install_cmd="${install_cmd//npm /sfw npm }"
|
||||
$install_cmd
|
||||
sfw_install_cmd="${install_cmd//npm /sfw npm }"
|
||||
|
||||
# Socket Firewall can fail to reach its own backend (a transient
|
||||
# network error unrelated to the actual npm install) and, in that
|
||||
# case, either exit non-zero or exit 0 without having actually
|
||||
# completed the install, leaving node_modules incomplete. We only
|
||||
# want to fall back to a plain (non-firewalled) install when we can
|
||||
# positively attribute the failure to *that* Socket Firewall error —
|
||||
# a real npm/package problem (bad version, checksum mismatch, failing
|
||||
# postinstall script, etc.) must fail the job as-is, not be silently
|
||||
# retried and potentially masked.
|
||||
sfw_log="$(mktemp)"
|
||||
set +e
|
||||
$sfw_install_cmd 2>&1 | tee "$sfw_log"
|
||||
sfw_exit="${PIPESTATUS[0]}"
|
||||
set -e
|
||||
|
||||
sfw_infra_failure=0
|
||||
if grep -qi 'Socket Firewall encountered an unexpected error' "$sfw_log"; then
|
||||
sfw_infra_failure=1
|
||||
fi
|
||||
|
||||
install_incomplete=0
|
||||
if [ "$sfw_exit" -eq 0 ]; then
|
||||
npm ls --depth=0 >/dev/null 2>&1 || install_incomplete=1
|
||||
fi
|
||||
rm -f "$sfw_log"
|
||||
|
||||
if [ "$sfw_infra_failure" -eq 1 ] && { [ "$sfw_exit" -ne 0 ] || [ "$install_incomplete" -eq 1 ]; }; then
|
||||
echo "::warning::Socket Firewall failed to reach its backend; retrying with a plain npm install"
|
||||
$install_cmd
|
||||
elif [ "$sfw_exit" -ne 0 ]; then
|
||||
echo "::error::npm install failed"
|
||||
exit "$sfw_exit"
|
||||
elif [ "$install_incomplete" -eq 1 ]; then
|
||||
echo "::error::npm install appears incomplete and is not attributable to a Socket Firewall failure"
|
||||
exit 1
|
||||
fi
|
||||
- name: Build packages
|
||||
if: ${{ inputs.run-build == 'true' }}
|
||||
shell: bash
|
||||
|
||||
@@ -73,6 +73,11 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/eslint-config-appium-ts/**']
|
||||
|
||||
- label: '@appium/oxc-config'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/oxc-config/**']
|
||||
|
||||
- label: '@appium/execute-driver-plugin'
|
||||
sync: true
|
||||
matcher:
|
||||
@@ -118,6 +123,11 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/schema/**']
|
||||
|
||||
- label: '@appium/semantic-release-config'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/semantic-release-config/**']
|
||||
|
||||
- label: '@appium/support'
|
||||
sync: true
|
||||
matcher:
|
||||
|
||||
+19
-19
@@ -15,10 +15,9 @@ on:
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- 'oxlint.config.mjs'
|
||||
- 'oxfmt.config.mjs'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
pull_request:
|
||||
@@ -35,10 +34,9 @@ on:
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- 'oxlint.config.mjs'
|
||||
- 'oxfmt.config.mjs'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
|
||||
@@ -61,7 +59,7 @@ jobs:
|
||||
outputs:
|
||||
packages: ${{ steps.discover-packages.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Discover packages with e2e tests
|
||||
id: discover-packages
|
||||
run: |
|
||||
@@ -103,14 +101,14 @@ jobs:
|
||||
|
||||
test-smoke:
|
||||
needs:
|
||||
- prepare_matrix
|
||||
- prepare_matrix
|
||||
name: Smoke Tests
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
@@ -122,14 +120,14 @@ jobs:
|
||||
|
||||
test-unit:
|
||||
needs:
|
||||
- prepare_matrix
|
||||
- prepare_matrix
|
||||
name: Unit Tests
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
@@ -141,14 +139,14 @@ jobs:
|
||||
|
||||
test-types:
|
||||
needs:
|
||||
- prepare_matrix
|
||||
- prepare_matrix
|
||||
name: Type Tests
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
@@ -160,8 +158,8 @@ jobs:
|
||||
|
||||
test-e2e:
|
||||
needs:
|
||||
- prepare_matrix
|
||||
- prepare_e2e_packages
|
||||
- prepare_matrix
|
||||
- prepare_e2e_packages
|
||||
name: E2E Tests - ${{ matrix.package_dir }} (Node ${{ matrix.node-version }})
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
strategy:
|
||||
@@ -171,7 +169,7 @@ jobs:
|
||||
package_dir: ${{ fromJSON(needs.prepare_e2e_packages.outputs.packages) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Setup, install and build
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
@@ -188,14 +186,16 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Setup and install (no build)
|
||||
uses: ./.github/actions/setup-build
|
||||
with:
|
||||
node-version: lts/*
|
||||
install-command: npm ci
|
||||
run-build: 'false'
|
||||
- name: ESLint
|
||||
- name: Lint
|
||||
run: npm run lint:ci
|
||||
- name: Prettier
|
||||
- name: Format check
|
||||
run: npm run format:check
|
||||
- name: Verify changelog generation
|
||||
run: npm run verify:changelog-generation
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -21,9 +21,9 @@ jobs:
|
||||
project_name: appium-inspector
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
CROWDIN_PROJECT_ID: ${{ vars[matrix.project_id_var] }}
|
||||
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
|
||||
- name: Post to Slack for ${{ matrix.project_name }}
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
uses: slackapi/slack-github-action@v4.0.0
|
||||
with:
|
||||
errors: true
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -12,11 +12,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
|
||||
@@ -15,9 +15,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
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@v2.0.2
|
||||
- name: Select all current LTS versions of Node.js
|
||||
id: generate-matrix
|
||||
uses: msimerson/node-lts-versions@v2.0.2
|
||||
|
||||
check:
|
||||
needs:
|
||||
- prepare_matrix
|
||||
- prepare_matrix
|
||||
name: Check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install Appium
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Install dependencies (Node.js)
|
||||
uses: bahmutov/npm-install@v1.12.1
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
env:
|
||||
FOSSA_API_KEY: ${{secrets.FOSSA_API_KEY}}
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
ref: master
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
# GITHUB_TOKEN is optional - if not provided, uses unauthenticated requests (rate limited)
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Post to Slack
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
uses: slackapi/slack-github-action@v4.0.0
|
||||
with:
|
||||
errors: true
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -8,6 +8,16 @@ on:
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
recover_missing_only:
|
||||
description: 'Recovery mode: skip version bump, publish only packages missing on npm at their current package.json version'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
bootstrap_packages:
|
||||
description: 'One-time beta-branch bootstrap only: comma-separated package names (e.g. @appium/base-driver,@appium/support) to force-bump to their next-major beta (premajor --preid beta), bypassing conventional-commit detection. Leave empty for a normal release.'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -18,11 +28,15 @@ permissions:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master' && contains(fromJSON('["mykola-mokhnach", "jlipps", "eglitise", "KazuCocoa"]'), github.actor)
|
||||
# keep this branch name in sync with env.BETA_BRANCH below - the `env` context isn't available
|
||||
# to jobs.<job_id>.if (GitHub Actions resolves it before job-level env is set up)
|
||||
if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/appium4') && contains(fromJSON('["mykola-mokhnach", "jlipps", "eglitise", "KazuCocoa"]'), github.actor)
|
||||
env:
|
||||
BETA_BRANCH: appium4
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -43,15 +57,148 @@ jobs:
|
||||
# for lerna createRelease
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
RECOVER_MISSING_ONLY: ${{ inputs.recover_missing_only }}
|
||||
BOOTSTRAP_PACKAGES: ${{ inputs.bootstrap_packages }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# On the beta branch ($BETA_BRANCH), every release is a beta prerelease anchored at its
|
||||
# target major (see packages/semantic-release-config's "Next-major beta branches" docs
|
||||
# for the equivalent semantic-release behavior this mirrors).
|
||||
BETA_FLAGS=""
|
||||
DIST_TAG_FLAGS=""
|
||||
if [ "${{ github.ref_name }}" = "$BETA_BRANCH" ]; then
|
||||
BETA_FLAGS="--conventional-prerelease --preid beta"
|
||||
DIST_TAG_FLAGS="--dist-tag beta"
|
||||
fi
|
||||
|
||||
# One-time bootstrap for packages just starting their beta train: force them
|
||||
# straight to <next-major>.0.0-beta.0 instead of waiting for/relying on a commit that
|
||||
# happens to be marked as breaking. Not part of the normal release flow below - this
|
||||
# exits early on its own.
|
||||
if [ -n "$BOOTSTRAP_PACKAGES" ]; then
|
||||
if [ "${{ github.ref_name }}" != "$BETA_BRANCH" ]; then
|
||||
echo "bootstrap_packages is only meaningful on the $BETA_BRANCH branch" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RECOVER_MISSING_ONLY" = "true" ]; then
|
||||
echo "bootstrap_packages cannot be combined with recover_missing_only" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Bootstrapping ${BOOTSTRAP_PACKAGES} to their next-major beta (premajor --preid beta), bypassing conventional-commit detection"
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
printf "n\n" | npx lerna version premajor --preid beta --exact --no-git-tag-version --no-push --force-publish="$BOOTSTRAP_PACKAGES"
|
||||
else
|
||||
npx lerna publish premajor --preid beta --exact --force-publish="$BOOTSTRAP_PACKAGES" --dist-tag beta --yes
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prints "<pkg_name>@<pkg_version>" to stdout for each non-private workspace package
|
||||
# whose current package.json version is not yet on npm.
|
||||
find_missing_packages() {
|
||||
for pkg_json in packages/*/package.json; do
|
||||
if [ ! -f "$pkg_json" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.private // false' "$pkg_json")" = "true" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
pkg_name="$(jq -r '.name // empty' "$pkg_json")"
|
||||
pkg_version="$(jq -r '.version // empty' "$pkg_json")"
|
||||
if [ -z "$pkg_name" ] || [ -z "$pkg_version" ]; then
|
||||
echo "Skipping malformed package manifest: $pkg_json" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
pkg_spec="${pkg_name}@${pkg_version}"
|
||||
if ! npm view "$pkg_spec" version >/dev/null 2>&1; then
|
||||
echo "$pkg_spec"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Resolves the workspace directory of a package given its npm name.
|
||||
package_dir_for() {
|
||||
local target="$1"
|
||||
for pkg_json in packages/*/package.json; do
|
||||
if [ ! -f "$pkg_json" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$(jq -r '.name // empty' "$pkg_json")" = "$target" ]; then
|
||||
dirname "$pkg_json"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Publishes each "<pkg_name>@<pkg_version>" package spec passed as an argument.
|
||||
# `lerna publish` has no per-package scope filter, so this runs `npm publish`
|
||||
# directly from each package's directory instead. Returns non-zero if any
|
||||
# individual publish failed; npm publish's own exit code is taken as authoritative,
|
||||
# no npm registry re-scan is done to double-check it.
|
||||
publish_missing_packages() {
|
||||
echo "Publishing missing packages one-by-one"
|
||||
local overall_status=0
|
||||
for pkg_spec in "$@"; do
|
||||
pkg_name="${pkg_spec%@*}"
|
||||
pkg_dir="$(package_dir_for "$pkg_name")"
|
||||
if [ -z "$pkg_dir" ]; then
|
||||
echo "Could not resolve workspace directory for ${pkg_name}"
|
||||
overall_status=1
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Publishing ${pkg_spec}"
|
||||
set +e
|
||||
(cd "$pkg_dir" && npm publish $DIST_TAG_FLAGS)
|
||||
publish_exit_code=$?
|
||||
set -e
|
||||
if [ $publish_exit_code -ne 0 ]; then
|
||||
echo "npm publish failed for ${pkg_spec} (exit code ${publish_exit_code})"
|
||||
overall_status=1
|
||||
fi
|
||||
done
|
||||
return $overall_status
|
||||
}
|
||||
|
||||
if [ "$RECOVER_MISSING_ONLY" = "true" ]; then
|
||||
echo "Recovery mode: scanning for packages missing on npm at their current package.json version"
|
||||
mapfile -t missing < <(find_missing_packages)
|
||||
|
||||
if [ ${#missing[@]} -eq 0 ]; then
|
||||
echo "No missing packages found; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Missing package versions on npm:"
|
||||
printf ' - %s\n' "${missing[@]}"
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "Dry run: not publishing"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! publish_missing_packages "${missing[@]}"; then
|
||||
echo "Recovery incomplete: one or more packages failed to publish"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All missing packages published successfully"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "Running lerna publish in dry-run mode"
|
||||
printf "n\n" | npx lerna version --exact --no-git-tag-version --no-push
|
||||
printf "n\n" | npx lerna version --exact --no-git-tag-version --no-push $BETA_FLAGS
|
||||
else
|
||||
echo "Running lerna publish for real"
|
||||
set +e
|
||||
release_output="$(npm run release -- --yes 2>&1)"
|
||||
release_output="$(npm run release -- --yes $BETA_FLAGS $DIST_TAG_FLAGS 2>&1)"
|
||||
release_exit_code=$?
|
||||
set -e
|
||||
printf '%s\n' "$release_output"
|
||||
@@ -76,55 +223,13 @@ jobs:
|
||||
fi
|
||||
|
||||
git checkout "$tagged_commit"
|
||||
missing=()
|
||||
for pkg_json in packages/*/package.json; do
|
||||
if [ ! -f "$pkg_json" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.private // false' "$pkg_json")" = "true" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
pkg_name="$(jq -r '.name // empty' "$pkg_json")"
|
||||
pkg_version="$(jq -r '.version // empty' "$pkg_json")"
|
||||
if [ -z "$pkg_name" ] || [ -z "$pkg_version" ]; then
|
||||
echo "Skipping malformed package manifest: $pkg_json"
|
||||
continue
|
||||
fi
|
||||
|
||||
pkg_spec="${pkg_name}@${pkg_version}"
|
||||
if ! npm view "$pkg_spec" version >/dev/null 2>&1; then
|
||||
missing+=("$pkg_spec")
|
||||
fi
|
||||
done
|
||||
mapfile -t missing < <(find_missing_packages)
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "Missing package versions on npm before scoped recovery:"
|
||||
printf ' - %s\n' "${missing[@]}"
|
||||
echo "Publishing missing packages one-by-one with scope"
|
||||
for pkg_spec in "${missing[@]}"; do
|
||||
pkg_name="${pkg_spec%@*}"
|
||||
echo "Publishing ${pkg_name}"
|
||||
set +e
|
||||
npx lerna publish from-package --yes --scope "$pkg_name"
|
||||
scoped_exit_code=$?
|
||||
set -e
|
||||
if [ $scoped_exit_code -ne 0 ]; then
|
||||
echo "Scoped publish exited non-zero for ${pkg_name}; will verify on npm"
|
||||
fi
|
||||
done
|
||||
|
||||
final_missing=()
|
||||
for pkg_spec in "${missing[@]}"; do
|
||||
if ! npm view "$pkg_spec" version >/dev/null 2>&1; then
|
||||
final_missing+=("$pkg_spec")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#final_missing[@]} -gt 0 ]; then
|
||||
echo "Recovery incomplete after scoped retries. Still missing:"
|
||||
printf ' - %s\n' "${final_missing[@]}"
|
||||
if ! publish_missing_packages "${missing[@]}"; then
|
||||
echo "Recovery incomplete: one or more packages failed to publish"
|
||||
exit $release_exit_code
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -15,11 +15,15 @@ permissions:
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master' && contains(fromJSON('["mykola-mokhnach", "jlipps", "eglitise", "KazuCocoa"]'), github.actor)
|
||||
# keep this branch name in sync with env.BETA_BRANCH below - the `env` context isn't available
|
||||
# to jobs.<job_id>.if (GitHub Actions resolves it before job-level env is set up)
|
||||
if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/appium4') && contains(fromJSON('["mykola-mokhnach", "jlipps", "eglitise", "KazuCocoa"]'), github.actor)
|
||||
env:
|
||||
BETA_BRANCH: appium4
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -38,4 +42,9 @@ jobs:
|
||||
- name: Run Lerna version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx lerna version --exact --yes
|
||||
run: |
|
||||
BETA_FLAGS=""
|
||||
if [ "${{ github.ref_name }}" = "$BETA_BRANCH" ]; then
|
||||
BETA_FLAGS="--conventional-prerelease --preid beta"
|
||||
fi
|
||||
npx lerna version --exact --yes $BETA_FLAGS
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for "On Hold" label
|
||||
uses: egmacke/action-check-label@v4
|
||||
uses: egmacke/action-check-label@v5
|
||||
with:
|
||||
label: On Hold
|
||||
state: absent
|
||||
|
||||
@@ -2,17 +2,18 @@ name: Publish docs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Appium Server Release"]
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
# Runs when any new Appium tag is pushed = there is a new release
|
||||
# Does not trigger for tags with a slash, e.g. 1.0/beta
|
||||
tags:
|
||||
- 'appium@*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- 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@v6.0.3
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Validate Renovate Config
|
||||
uses: rinchsan/renovate-config-validator@v0.2.1
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codegraph": {
|
||||
"type": "stdio",
|
||||
"command": "codegraph",
|
||||
"args": ["serve", "--mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
require: ['ts-node/register'],
|
||||
// forbids use of .only() in CI
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
color: true,
|
||||
timeout: '5s',
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
**/node_modules/**
|
||||
**/build/**
|
||||
**/fixtures/**
|
||||
**/*.min.*
|
||||
**/*.md
|
||||
package-lock.json
|
||||
package.json
|
||||
**/.vscode/**
|
||||
**/*.html
|
||||
**/generated/**
|
||||
# generated
|
||||
packages/schema/lib/appium-config.schema.json
|
||||
packages/types/lib/appium-config.ts
|
||||
**/*.hbs
|
||||
|
||||
/.nx/workspace-data
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>appium/appium//renovate/default"],
|
||||
"schedule": [
|
||||
"after 10pm",
|
||||
"before 5:00am"
|
||||
],
|
||||
"schedule": ["after 10pm", "before 5:00am"],
|
||||
"timezone": "America/Vancouver"
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ to driver/client documentation
|
||||
* [fake-plugin](packages/fake-plugin/CHANGELOG.md)
|
||||
* [logger](packages/logger/CHANGELOG.md)
|
||||
* [opencv](packages/opencv/CHANGELOG.md)
|
||||
* [oxc-config](packages/oxc-config/CHANGELOG.md)
|
||||
* [plugin-test-support](packages/plugin-test-support/CHANGELOG.md)
|
||||
* [schema](packages/schema/CHANGELOG.md)
|
||||
* [semantic-release-config](packages/semantic-release-config/CHANGELOG.md)
|
||||
* [strongbox](packages/strongbox/CHANGELOG.md)
|
||||
* [support](packages/support/CHANGELOG.md)
|
||||
* [tsconfig](packages/tsconfig/CHANGELOG.md)
|
||||
|
||||
+2
-5
@@ -7,10 +7,7 @@ import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
const testFileConfigItems = appiumConfig.filter(
|
||||
(item) => typeof item?.name === 'string' && item.name.startsWith('Test Files'),
|
||||
);
|
||||
const testSupportFiles = [
|
||||
'packages/driver-test-support/lib/**',
|
||||
'packages/plugin-test-support/lib/**',
|
||||
];
|
||||
const testSupportFiles = ['packages/driver-test-support/lib/**', 'packages/plugin-test-support/lib/**'];
|
||||
const testSupportConfig = testFileConfigItems.map((item) => ({...item, files: testSupportFiles}));
|
||||
|
||||
export default defineConfig([
|
||||
@@ -28,7 +25,7 @@ export default defineConfig([
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ fixStyle: 'separate-type-imports', prefer: 'type-imports' },
|
||||
{fixStyle: 'separate-type-imports', prefer: 'type-imports'},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"version": {
|
||||
"conventionalCommits": true,
|
||||
"createRelease": "github",
|
||||
"allowBranch": ["master", "appium3"],
|
||||
"allowBranch": ["master", "appium4"],
|
||||
"exact": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import appiumConfig, {defineConfig, ignorePatterns as appiumIgnorePatterns} from '@appium/oxc-config/oxfmt';
|
||||
|
||||
export default defineConfig({
|
||||
...appiumConfig,
|
||||
ignorePatterns: [
|
||||
...appiumIgnorePatterns,
|
||||
'packages/appium/docs/**',
|
||||
'packages/schema/lib/appium-config.schema.json',
|
||||
'packages/types/lib/appium-config.ts',
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import appiumConfig, {defineConfig, ignorePatterns as appiumIgnorePatterns} from '@appium/oxc-config/oxlint';
|
||||
|
||||
export default defineConfig({
|
||||
extends: [appiumConfig],
|
||||
ignorePatterns: [
|
||||
...appiumIgnorePatterns,
|
||||
'packages/appium/docs/**/assets/**',
|
||||
'packages/appium/docs/**/js/**',
|
||||
'packages/appium/sample-code/**',
|
||||
],
|
||||
});
|
||||
Generated
+7743
-3950
File diff suppressed because it is too large
Load Diff
+36
-62
@@ -4,28 +4,31 @@
|
||||
"private": true,
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"android",
|
||||
"automation",
|
||||
"firefoxos",
|
||||
"ios",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
"testing",
|
||||
"webdriver"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s build:*",
|
||||
"build:compile": "tsc -b",
|
||||
@@ -47,11 +50,11 @@
|
||||
"docs:publish": "npm run -w packages/appium publish:docs",
|
||||
"doctor": "node ./scripts/doctor-stub.mjs",
|
||||
"install-docs-deps": "npm run -w packages/appium install-docs-deps",
|
||||
"format": "prettier -w ./packages ./scripts",
|
||||
"format:check": "prettier --check ./packages ./scripts",
|
||||
"lint": "eslint .",
|
||||
"lint:ci": "eslint --quiet .",
|
||||
"lint:fix": "run-s \"lint -- --fix\"",
|
||||
"format": "oxfmt -c oxfmt.config.mjs .",
|
||||
"format:check": "oxfmt -c oxfmt.config.mjs --check .",
|
||||
"lint": "oxlint -c oxlint.config.mjs .",
|
||||
"lint:ci": "oxlint -c oxlint.config.mjs --quiet .",
|
||||
"lint:fix": "oxlint -c oxlint.config.mjs --fix .",
|
||||
"prepare": "run-p rebuild",
|
||||
"prepublishOnly": "run-s rebuild",
|
||||
"preversion": "run-s rebuild",
|
||||
@@ -68,90 +71,57 @@
|
||||
"test:e2e": "lerna run test:e2e",
|
||||
"test:quick": "run-s lint test:unit test:types",
|
||||
"test:slow": "run-s test:quick test:smoke test:e2e",
|
||||
"test:smoke": "smoker --all test:smoke",
|
||||
"test:smoke": "lerna run test:smoke",
|
||||
"test:types": "lerna run test:types",
|
||||
"test:unit": "lerna run test",
|
||||
"upload": "gulp github-upload",
|
||||
"verify:changelog-generation": "node ./scripts/verify-changelog-generation.mjs",
|
||||
"zip": "zip -qr ./appium.zip ./packages/appium",
|
||||
"zip-and-upload": "run-s zip upload"
|
||||
},
|
||||
"prettier": {
|
||||
"bracketSpacing": false,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"@stylistic/eslint-plugin": "5.10.0",
|
||||
"@tsconfig/node20": "20.1.9",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@appium/oxc-config": "1.0.0",
|
||||
"@tsconfig/node20": "20.1.10",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/chai": "5.2.3",
|
||||
"@types/chai-as-promised": "8.0.2",
|
||||
"@types/content-disposition": "0.5.9",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/finalhandler": "1.2.4",
|
||||
"@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.24",
|
||||
"@types/method-override": "3.0.0",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/morgan": "1.9.10",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/node": "24.13.2",
|
||||
"@types/node": "24.13.3",
|
||||
"@types/plist": "3.0.5",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/semver": "7.8.0",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@types/sinon": "21.0.1",
|
||||
"@types/sinon-chai": "4.0.0",
|
||||
"@types/sinon": "22.0.0",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.35",
|
||||
"asyncbox": "6.3.0",
|
||||
"chai": "6.2.2",
|
||||
"chai-as-promised": "8.0.2",
|
||||
"conventional-changelog-conventionalcommits": "7.0.2",
|
||||
"asyncbox": "6.4.2",
|
||||
"conventional-changelog-conventionalcommits": "9.3.1",
|
||||
"cpy-cli": "7.0.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint": "10.5.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-typescript": "4.4.5",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-mocha": "11.3.0",
|
||||
"eslint-plugin-n": "18.1.0",
|
||||
"eslint-plugin-promise": "7.3.0",
|
||||
"finalhandler": "2.1.1",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "9.0.7",
|
||||
"midnight-smoker": "8.0.0",
|
||||
"lerna": "10.0.1",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
"mjpeg-server": "0.3.1",
|
||||
"mocha": "11.7.6",
|
||||
"npm-run-all2": "9.0.2",
|
||||
"prettier": "3.8.4",
|
||||
"npm-run-all2": "9.0.3",
|
||||
"rewiremock": "3.14.6",
|
||||
"rimraf": "6.1.3",
|
||||
"serve-static": "2.2.1",
|
||||
"sinon": "22.0.0",
|
||||
"sinon-chai": "4.0.1",
|
||||
"teen_process": "4.1.3",
|
||||
"ts-node": "10.9.2",
|
||||
"sinon": "22.1.0",
|
||||
"teen_process": "4.2.1",
|
||||
"tsd": "0.33.0",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"webdriverio": "9.28.0",
|
||||
"ws": "8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"webdriverio": "9.31.1",
|
||||
"ws": "8.21.3"
|
||||
},
|
||||
"overrides": {
|
||||
"@sidvind/better-ajv-errors": {
|
||||
@@ -161,5 +131,9 @@
|
||||
"ajv": "8.20.0"
|
||||
},
|
||||
"http-cache-semantics": "4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
require: ['ts-node/register'],
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
color: true,
|
||||
timeout: '5s',
|
||||
};
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.7.0](https://github.com/appium/appium/compare/appium@3.6.0...appium@3.7.0) (2026-08-24)
|
||||
|
||||
### Features
|
||||
|
||||
* **base-driver:** add httpServer.frontRouter for global extension middleware ([#22632](https://github.com/appium/appium/issues/22632)) ([f06226b](https://github.com/appium/appium/commit/f06226b7bb2fb7ee2f3a61833f4a1d7777d4ef21))
|
||||
* **storage-plugin:** Add route aliases ([#22558](https://github.com/appium/appium/issues/22558)) ([a551c38](https://github.com/appium/appium/commit/a551c3817156bbc7e06793227643a2d11c0c0259))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** load @appium/docutils dynamically in docs build script ([#22584](https://github.com/appium/appium/issues/22584)) ([2c0f3fd](https://github.com/appium/appium/commit/2c0f3fd9e319c3bc774bdd2a65148da11094c87c))
|
||||
* **appium:** parse --webhook as a URI instead of host:port ([#22635](https://github.com/appium/appium/issues/22635)) ([c35a12a](https://github.com/appium/appium/commit/c35a12a51d30fe311ef96ebe386cd9c5540e3927))
|
||||
* **support:** do not treat a sibling folder as a subpath of the root ([#22586](https://github.com/appium/appium/issues/22586)) ([3a80498](https://github.com/appium/appium/commit/3a8049887dc673a6658c3821a2882e0145c3aa86))
|
||||
|
||||
|
||||
## [3.6.0](https://github.com/appium/appium/compare/appium@3.5.2...appium@3.6.0) (2026-07-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** add --allow-unknown-args to tolerate unrecognized server CLI arguments ([#22477](https://github.com/appium/appium/issues/22477)) ([f943fab](https://github.com/appium/appium/commit/f943fab1411826212cfb174e3847e610c25f37b7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** Duplicated module import ([#22519](https://github.com/appium/appium/issues/22519)) ([154ea18](https://github.com/appium/appium/commit/154ea1825281335569f0d6952db527135c75e153))
|
||||
* typescript config ([#22540](https://github.com/appium/appium/issues/22540)) ([ea6a2e1](https://github.com/appium/appium/commit/ea6a2e117f56c1d7ff459378700494407c51608e))
|
||||
* Typescript references ([#22458](https://github.com/appium/appium/issues/22458)) ([fd70da7](https://github.com/appium/appium/commit/fd70da7ab407bf1f6fec8a3576b9a33f0aef9b69))
|
||||
|
||||
|
||||
|
||||
## [3.5.2](https://github.com/appium/appium/compare/appium@3.5.1...appium@3.5.2) (2026-06-18)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.5.1](https://github.com/appium/appium/compare/appium@3.5.0...appium@3.5.1) (2026-06-18)
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ plugins:
|
||||
blog:
|
||||
enabled: true
|
||||
post_excerpt: required
|
||||
mike:
|
||||
canonical_version: 'latest'
|
||||
redirects:
|
||||
redirect_maps:
|
||||
'intro/requirements.md': 'quickstart/requirements.md'
|
||||
|
||||
@@ -8,11 +8,13 @@ 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 -->
|
||||
|
||||
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.
|
||||
|
||||
<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" />
|
||||
@@ -22,15 +24,19 @@ This kind of hands-on contribution is crucial for ensuring Appium continues to e
|
||||
|
||||
|
||||
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.
|
||||
|
||||
* [`appium-mcp`](https://github.com/appium/appium-mcp) – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
* [`appium-ios-remotexpc`](https://github.com/appium/appium-ios-remotexpc) – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
* [`appium-ios-tuntap`](https://github.com/appium/appium-ios-tuntap) – 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
|
||||
|
||||
**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!
|
||||
|
||||
@@ -222,17 +222,22 @@ So you'll probably end up overriding `createSession`. You can do so by defining
|
||||
driver:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData) {
|
||||
const [sessionId, caps] = super.createSession(w3cCaps);
|
||||
async createSession(w3cCaps) {
|
||||
const [sessionId, caps] = await super.createSession(w3cCaps);
|
||||
// do your own stuff here
|
||||
return [sessionId, caps];
|
||||
}
|
||||
```
|
||||
|
||||
For legacy reasons, your function will receive old-style JSON Wire Protocol desired and required
|
||||
caps as the first two arguments. Given that the old protocol isn't supported anymore and clients
|
||||
have all been updated, you can instead only rely on the `w3cCaps` parameter. (For a discussion
|
||||
about what `otherDriverData` is about, see the section below on concurrent drivers).
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Older drivers may still define `createSession` with up to four parameters, e.g.
|
||||
`createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData)`. This shape is a holdover from the
|
||||
retired JSON Wire Protocol, which required desired and required caps as the first two
|
||||
arguments; only the first W3C-shaped argument was ever used. The `otherDriverData` parameter is
|
||||
likewise deprecated in favor of [IPC](#send-messages-to-plugins-running-on-the-same-session) for
|
||||
coordinating with other concurrently-running sessions. New drivers should use the single-argument
|
||||
form shown above.
|
||||
|
||||
You'll want to make sure to call `super.createSession` in order to get the session ID as well as
|
||||
the processed capabilities (note that capabilities are also set on `this.caps`; modifying `caps`
|
||||
@@ -520,14 +525,13 @@ route, then the route will not be proxied and instead will be handled by your dr
|
||||
example, we are avoiding proxying all `POST` routes that have the `appium` prefix.
|
||||
|
||||
Next, we have to set up the proxying itself. The way to do this is to use a special class from
|
||||
Appium called `JWProxy`. (The name means "JSON Wire Proxy" and is related to a legacy
|
||||
implementation of the protocol). You'll want to create a `JWProxy` object using the details required to
|
||||
connect to the remote server:
|
||||
Appium called `WebDriverProxy`. You'll want to create a `WebDriverProxy` object using the details
|
||||
required to connect to the remote server:
|
||||
|
||||
```js
|
||||
// import {JWProxy} from 'appium/driver';
|
||||
// import {WebDriverProxy} from 'appium/driver';
|
||||
|
||||
const proxy = new JWProxy({
|
||||
const proxy = new WebDriverProxy({
|
||||
server: 'remote.server',
|
||||
port: 1234,
|
||||
base: '/',
|
||||
@@ -539,8 +543,8 @@ this.proxyCommand = proxy.command.bind(proxy);
|
||||
|
||||
Here we are creating a proxy object and assigning some of its methods to `this` under the names
|
||||
`proxyReqRes` and `proxyCommand`. This is required for Appium to use the proxy, so don't forget
|
||||
this step! The `JWProxy` has a variety of other options which you can check out in the source code,
|
||||
as well. (TODO: publish options as API docs and link here).
|
||||
this step! The `WebDriverProxy` has a variety of other options [which you can check out in the
|
||||
source code as well.](https://github.com/appium/appium/blob/master/packages/base-driver/lib/jsonwp-proxy/proxy.ts)
|
||||
|
||||
Finally, we need a way to tell Appium when the proxy is active. For your driver it might always
|
||||
be active, or it might only be active when in a certain context. You can define the logic as an
|
||||
@@ -797,6 +801,12 @@ that multiple simultaneous sessions don't use the same resources:
|
||||
1. Have each driver express what resources it is using, then examine currently-used resources from
|
||||
other drivers when a new session begins.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
The `driverData`-based mechanism described below is deprecated. Prefer
|
||||
[IPC](#send-messages-to-plugins-running-on-the-same-session) for coordinating resources across
|
||||
concurrently-running sessions instead.
|
||||
|
||||
To support this third strategy, you can implement `get driverData` in your driver to return what
|
||||
sorts of resources your driver is currently using, for example:
|
||||
|
||||
@@ -808,7 +818,7 @@ get driverData() {
|
||||
|
||||
Now, when a new session is started on your driver, the `driverData` response from any other
|
||||
simultaneously running drivers (of the same type) will also be included, as the last parameter of
|
||||
the `createSession` method:
|
||||
the deprecated, multi-argument form of the `createSession` method:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, driverData)
|
||||
|
||||
@@ -477,9 +477,9 @@ capability.
|
||||
You probably don't normally need to update the Appium server object (which is an
|
||||
[Express](https://expressjs.com/) server having already been
|
||||
[configured](https://github.com/appium/appium/blob/master/packages/base-driver/lib/express/server.js)
|
||||
in a variety of ways). But, for example, you could add new Express middleware to the server to
|
||||
support your plugin's requirements. To update the server you must implement the `static async
|
||||
updateServer` method in your class. This method takes three parameters:
|
||||
in a variety of ways). But, for example, you could add new routes to the server to support your
|
||||
plugin's requirements. To update the server you must implement the `static async updateServer`
|
||||
method in your class. This method takes three parameters:
|
||||
|
||||
* `expressApp`: the Express app object
|
||||
* `httpServer`: the Node HTTP server object
|
||||
@@ -492,6 +492,25 @@ results you'll need to test! Warning: this should be considered an advanced feat
|
||||
knowledge of Express, as well as the care not to do anything that could affect the operation of
|
||||
other parts of the Appium server!
|
||||
|
||||
`updateServer` runs *after* Appium's own routes are registered, so `expressApp.use(...)` here
|
||||
won't see requests to paths Appium already owns — Express matches middleware and routes in
|
||||
registration order. For middleware that must see requests to those routes too (e.g. logging or
|
||||
auth), use `httpServer.frontRouter` instead: Appium mounts this
|
||||
[Router](https://expressjs.com/en/5x/api/router/) before any route is registered, so anything
|
||||
attached to it — even from inside `updateServer` — still runs ahead of route matching. Note this
|
||||
is still *after* Appium's own baseline middleware (CORS, WebSocket upgrade handling, body
|
||||
parsing, etc.), so it won't see requests Appium itself has already rejected, e.g. a CORS
|
||||
preflight failure or a body that failed to parse:
|
||||
|
||||
```js
|
||||
static async updateServer(expressApp, httpServer, cliArgs) {
|
||||
httpServer.frontRouter.use((req, res, next) => {
|
||||
console.log(`Incoming request: ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Handle unexpected session shutdown
|
||||
|
||||
When developing a plugin you may want to add some cleanup logic for when a session ends. You would
|
||||
|
||||
@@ -126,7 +126,7 @@ appium driver install --source=npm appium-flutter-integration-driver
|
||||
appium driver install --source=npm appium-lg-webos-driver
|
||||
```
|
||||
|
||||
### [Linux](https://github.com/fantonglang/appium-linux-driver)
|
||||
### [Linux (@fantonglang)](https://github.com/fantonglang/appium-linux-driver)
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -143,6 +143,16 @@ yarn install
|
||||
node ./
|
||||
```
|
||||
|
||||
### [Linux (@Itsmeaj)](https://github.com/Itsmeaj/appium)
|
||||
|
||||
* Target: Linux desktop applications on X11 and Wayland
|
||||
* Mode: Native
|
||||
* Supported by: `@Itsmeaj`
|
||||
|
||||
```sh title="Install This Driver"
|
||||
appium driver install --source=npm @itsmeaj/appium-linux-driver
|
||||
```
|
||||
|
||||
### [NovaWindows](https://github.com/AutomateThePlanet/appium-novawindows-driver)
|
||||
|
||||
!!! info
|
||||
|
||||
@@ -106,6 +106,48 @@ Retrieves the [execute methods](../../guides/execute-methods.md) supported in th
|
||||
origin (driver-specific or plugin-specific). Refer to [the type definition file](https://github.com/appium/appium/blob/master/packages/types/lib/command.ts)
|
||||
for a detailed structure of this object.
|
||||
|
||||
### getCurrentAppiumContext
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
|
||||
### setAppiumContext
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
|--|--|--|
|
||||
|`name`|Name of the context to set as the active one|string|
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumContexts
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/contexts
|
||||
```
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
### getLogEvents
|
||||
|
||||
```
|
||||
@@ -392,3 +434,71 @@ Retrieves data from a directory on the device under test.
|
||||
#### Response
|
||||
|
||||
`string` - the Base64-encoded zip file of the directory contents
|
||||
|
||||
### getAppiumRotation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
|
||||
|Name|Description|Type|
|
||||
|--|--|--|
|
||||
|`x`|Degrees by which the device is rotated on its X axis|number|
|
||||
|`y`|Degrees by which the device is rotated on its Y axis|number|
|
||||
|`z`|Degrees by which the device is rotated on its Z axis|number|
|
||||
|
||||
### setAppiumRotation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
|--|--|--|
|
||||
|`x`|Degrees by which the device is rotated on its X axis|number|
|
||||
|`y`|Degrees by which the device is rotated on its Y axis|number|
|
||||
|`z`|Degrees by which the device is rotated on its Z axis|number|
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumOrientation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
|
||||
### setAppiumOrientation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
|--|--|--|
|
||||
|`orientation`|New device orientation. Supported values are `PORTRAIT` or `LANDSCAPE`.|string|
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
@@ -139,6 +139,10 @@ GET /session/:sessionId/orientation
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumOrientation`](./appium.md#getappiumorientation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
@@ -153,6 +157,10 @@ POST /session/:sessionId/orientation
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumOrientation`](./appium.md#setappiumorientation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
|
||||
@@ -20,6 +20,10 @@ GET /session/:sessionId/rotation
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumRotation`](./appium.md#getappiumrotation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
@@ -40,6 +44,10 @@ POST /session/:sessionId/rotation
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumRotation`](./appium.md#setappiumrotation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
@@ -62,6 +70,10 @@ GET /session/:sessionId/context
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getCurrentAppiumContext`](./appium.md#getcurrentappiumcontext) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
@@ -76,6 +88,10 @@ POST /session/:sessionId/context
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumContext`](./appium.md#setappiumcontext) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
|Name|Description|Type|
|
||||
@@ -96,6 +112,10 @@ GET /session/:sessionId/contexts
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumContexts`](./appium.md#getappiumcontexts) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
@@ -169,10 +169,17 @@ Modifies the [`createSession`](./webdriver.md#createsession) endpoint:
|
||||
All endpoints for this plugin can be invoked without creating a session, allowing you to prepare
|
||||
your test environment in advance.
|
||||
|
||||
!!! info
|
||||
|
||||
Prior to plugin version 1.2.0, all endpoints below were mounted under the `/storage` prefix
|
||||
(e.g. `/storage/add`) instead of `/appium/storage`. The `/storage`-prefixed routes are still
|
||||
available for backward compatibility, but are deprecated and will be removed in a future
|
||||
version of the plugin.
|
||||
|
||||
### addStorageItem
|
||||
|
||||
```
|
||||
POST /storage/add
|
||||
POST /appium/storage/add
|
||||
```
|
||||
|
||||
Adds a new file to the storage.
|
||||
@@ -198,8 +205,8 @@ Example:
|
||||
```json
|
||||
{
|
||||
"ws": {
|
||||
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
"stream": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
},
|
||||
"ttlMs": 300000
|
||||
}
|
||||
@@ -208,7 +215,7 @@ Example:
|
||||
### deleteStorageItem
|
||||
|
||||
```
|
||||
POST /storage/delete
|
||||
POST /appium/storage/delete
|
||||
```
|
||||
|
||||
Deletes a file in the storage.
|
||||
@@ -227,7 +234,7 @@ the storage, or if the delete request is invalid.
|
||||
### listStorageItems
|
||||
|
||||
```
|
||||
GET /storage/list
|
||||
GET /appium/storage/list
|
||||
```
|
||||
|
||||
List all files present in the storage.
|
||||
@@ -245,7 +252,7 @@ List all files present in the storage.
|
||||
### resetStorage
|
||||
|
||||
```
|
||||
POST /storage/reset
|
||||
POST /appium/storage/reset
|
||||
```
|
||||
|
||||
Deletes all uploaded files and stops any incomplete uploads.
|
||||
|
||||
@@ -158,13 +158,13 @@ extensions include scripts.
|
||||
#### Usage
|
||||
|
||||
```
|
||||
appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
|
||||
```
|
||||
|
||||
|Argument|Description|
|
||||
|--|--|
|
||||
|`extension-name`|The short name of the installed extension|
|
||||
|`script-name`|The name of the script to be run|
|
||||
|`script-name`|The name of the script to run. If not provided, a list of available scripts is returned.|
|
||||
|`script-args`|Any additional arguments passed to the script|
|
||||
|
||||
#### Options
|
||||
@@ -181,6 +181,12 @@ appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium driver run uiautomator2 reset
|
||||
```
|
||||
|
||||
- List all available scripts included in the XCUITest driver:
|
||||
|
||||
```
|
||||
appium driver run xcuitest
|
||||
```
|
||||
|
||||
## `update`
|
||||
|
||||
Updates one or more extensions. Only supported for extensions installed via `npm`. By default,
|
||||
|
||||
@@ -29,6 +29,7 @@ appium
|
||||
|`--address`, `-a`|IPv4/IPv6 address to listen on|string|`0.0.0.0`|
|
||||
|`--allow-cors`|Allow web browser connections from any host|boolean|`false`|
|
||||
|`--allow-insecure`|List of [insecure features](../../guides/security.md) that should be allowed in this server's sessions. Individual features can be overridden by `--deny-insecure`. Has no effect in combination with `--relaxed-security`.|array<string>|`[]`|
|
||||
|`--allow-unknown-args`|Do not exit if unrecognized command-line arguments are passed to the server; ignore them instead. Useful when the Appium CLI is wrapped by external tooling that appends extra flags.|boolean|`false`|
|
||||
|`--base-path`, `-pa`|Base path to use as the prefix for all webdriver routes running on the server|string|`""`|
|
||||
|`--callback-address`, `-ca`|Callback IP address|string|`0.0.0.0`|
|
||||
|`--callback-port`, `-cp`|Callback port|integer|`4723`|
|
||||
@@ -64,7 +65,7 @@ appium
|
||||
|`--tmp`|Absolute path to the directory used for temporary files|string|[`os.tmpdir()`](https://nodejs.org/api/os.html#ostmpdir)|
|
||||
|`--use-drivers`|List of drivers to activate. By default, all installed drivers are activated.|array<string>|`[]`|
|
||||
|`--use-plugins`|List of plugins to activate. By default, no plugins are activated. Set to `["all"]` to activate all installed plugins.|array<string>|`[]`|
|
||||
|`--webhook`, `-G`|URL for an HTTP listener where the server logs should be output. This does not affect output on the console.|string||
|
||||
|`--webhook`, `-G`|URL for an HTTP listener where the server logs should be output. This does not affect output on the console. A bare `host:port` is also accepted, which posts the logs to the root path over plain http; a value having neither a scheme nor a port falls back to `127.0.0.1:9003`.|string||
|
||||
|
||||
### Info Options
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ title: Additional Resources
|
||||
|
||||
Here you can find links to additional Appium resources around the web:
|
||||
|
||||
## Websites
|
||||
|
||||
- [Appium Pro](https://appiumpro.com) - a blog and newsletter written by one of Appium's
|
||||
maintainers, Jonathan Lipps, with lots of useful guides
|
||||
|
||||
## Online Courses
|
||||
|
||||
- [Appium and Selenium Fundamentals](https://ui.headspin.io/university/learn/appium-selenium-fundamentals-2020) - a comprehensive video course on learning Python, Selenium, and Appium by Jonathan Lipps
|
||||
|
||||
@@ -8,11 +8,13 @@ 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 -->
|
||||
|
||||
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.
|
||||
|
||||
<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" />
|
||||
@@ -20,15 +22,19 @@ This kind of hands-on contribution is crucial for ensuring Appium continues to e
|
||||
</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.
|
||||
|
||||
- [`appium-mcp`](https://github.com/appium/appium-mcp) – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
- [`appium-ios-remotexpc`](https://github.com/appium/appium-ios-remotexpc) – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
- [`appium-ios-tuntap`](https://github.com/appium/appium-ios-tuntap) – 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
|
||||
|
||||
**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!
|
||||
|
||||
@@ -222,17 +222,24 @@ So you'll probably end up overriding `createSession`. You can do so by defining
|
||||
driver:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData) {
|
||||
const [sessionId, caps] = super.createSession(w3cCaps);
|
||||
async createSession(w3cCaps) {
|
||||
const [sessionId, caps] = await super.createSession(w3cCaps);
|
||||
// do your own stuff here
|
||||
return [sessionId, caps];
|
||||
}
|
||||
```
|
||||
|
||||
For legacy reasons, your function will receive old-style JSON Wire Protocol desired and required
|
||||
caps as the first two arguments. Given that the old protocol isn't supported anymore and clients
|
||||
have all been updated, you can instead only rely on the `w3cCaps` parameter. (For a discussion
|
||||
about what `otherDriverData` is about, see the section below on concurrent drivers).
|
||||
!!! warning "Deprecated"
|
||||
|
||||
```
|
||||
Older drivers may still define `createSession` with up to four parameters, e.g.
|
||||
`createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData)`. This shape is a holdover from the
|
||||
retired JSON Wire Protocol, which required desired and required caps as the first two
|
||||
arguments; only the first W3C-shaped argument was ever used. The `otherDriverData` parameter is
|
||||
likewise deprecated in favor of [IPC](#send-messages-to-plugins-running-on-the-same-session) for
|
||||
coordinating with other concurrently-running sessions. New drivers should use the single-argument
|
||||
form shown above.
|
||||
```
|
||||
|
||||
You'll want to make sure to call `super.createSession` in order to get the session ID as well as
|
||||
the processed capabilities (note that capabilities are also set on `this.caps`; modifying `caps`
|
||||
@@ -522,14 +529,13 @@ route, then the route will not be proxied and instead will be handled by your dr
|
||||
example, we are avoiding proxying all `POST` routes that have the `appium` prefix.
|
||||
|
||||
Next, we have to set up the proxying itself. The way to do this is to use a special class from
|
||||
Appium called `JWProxy`. (The name means "JSON Wire Proxy" and is related to a legacy
|
||||
implementation of the protocol). You'll want to create a `JWProxy` object using the details required to
|
||||
connect to the remote server:
|
||||
Appium called `WebDriverProxy`. You'll want to create a `WebDriverProxy` object using the details
|
||||
required to connect to the remote server:
|
||||
|
||||
```js
|
||||
// import {JWProxy} from 'appium/driver';
|
||||
// import {WebDriverProxy} from 'appium/driver';
|
||||
|
||||
const proxy = new JWProxy({
|
||||
const proxy = new WebDriverProxy({
|
||||
server: 'remote.server',
|
||||
port: 1234,
|
||||
base: '/',
|
||||
@@ -541,8 +547,8 @@ this.proxyCommand = proxy.command.bind(proxy);
|
||||
|
||||
Here we are creating a proxy object and assigning some of its methods to `this` under the names
|
||||
`proxyReqRes` and `proxyCommand`. This is required for Appium to use the proxy, so don't forget
|
||||
this step! The `JWProxy` has a variety of other options which you can check out in the source code,
|
||||
as well. (TODO: publish options as API docs and link here).
|
||||
this step! The `WebDriverProxy` has a variety of other options which you can check out in the
|
||||
source code as well.
|
||||
|
||||
Finally, we need a way to tell Appium when the proxy is active. For your driver it might always
|
||||
be active, or it might only be active when in a certain context. You can define the logic as an
|
||||
@@ -799,6 +805,14 @@ that multiple simultaneous sessions don't use the same resources:
|
||||
3. Have each driver express what resources it is using, then examine currently-used resources from
|
||||
other drivers when a new session begins.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
```
|
||||
The `driverData`-based mechanism described below is deprecated. Prefer
|
||||
[IPC](#send-messages-to-plugins-running-on-the-same-session) for coordinating resources across
|
||||
concurrently-running sessions instead.
|
||||
```
|
||||
|
||||
To support this third strategy, you can implement `get driverData` in your driver to return what
|
||||
sorts of resources your driver is currently using, for example:
|
||||
|
||||
@@ -810,7 +824,7 @@ get driverData() {
|
||||
|
||||
Now, when a new session is started on your driver, the `driverData` response from any other
|
||||
simultaneously running drivers (of the same type) will also be included, as the last parameter of
|
||||
the `createSession` method:
|
||||
the deprecated, multi-argument form of the `createSession` method:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, driverData)
|
||||
|
||||
@@ -483,9 +483,9 @@ capability.
|
||||
You probably don't normally need to update the Appium server object (which is an
|
||||
[Express](https://expressjs.com/) server having already been
|
||||
[configured](https://github.com/appium/appium/blob/master/packages/base-driver/lib/express/server.js)
|
||||
in a variety of ways). But, for example, you could add new Express middleware to the server to
|
||||
support your plugin's requirements. To update the server you must implement the `static async
|
||||
updateServer` method in your class. This method takes three parameters:
|
||||
in a variety of ways). But, for example, you could add new routes to the server to support your
|
||||
plugin's requirements. To update the server you must implement the `static async updateServer`
|
||||
method in your class. This method takes three parameters:
|
||||
|
||||
- `expressApp`: the Express app object
|
||||
- `httpServer`: the Node HTTP server object
|
||||
@@ -498,6 +498,25 @@ results you'll need to test! Warning: this should be considered an advanced feat
|
||||
knowledge of Express, as well as the care not to do anything that could affect the operation of
|
||||
other parts of the Appium server!
|
||||
|
||||
`updateServer` runs _after_ Appium's own routes are registered, so `expressApp.use(...)` here
|
||||
won't see requests to paths Appium already owns — Express matches middleware and routes in
|
||||
registration order. For middleware that must see requests to those routes too (e.g. logging or
|
||||
auth), use `httpServer.frontRouter` instead: Appium mounts this
|
||||
[Router](https://expressjs.com/en/5x/api/router/) before any route is registered, so anything
|
||||
attached to it — even from inside `updateServer` — still runs ahead of route matching. Note this
|
||||
is still _after_ Appium's own baseline middleware (CORS, WebSocket upgrade handling, body
|
||||
parsing, etc.), so it won't see requests Appium itself has already rejected, e.g. a CORS
|
||||
preflight failure or a body that failed to parse:
|
||||
|
||||
```js
|
||||
static async updateServer(expressApp, httpServer, cliArgs) {
|
||||
httpServer.frontRouter.use((req, res, next) => {
|
||||
console.log(`Incoming request: ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Handle unexpected session shutdown
|
||||
|
||||
When developing a plugin you may want to add some cleanup logic for when a session ends. You would
|
||||
|
||||
@@ -107,6 +107,48 @@ Retrieves the [execute methods](../../guides/execute-methods.md) supported in th
|
||||
origin (driver-specific or plugin-specific). Refer to [the type definition file](https://github.com/appium/appium/blob/master/packages/types/lib/command.ts)
|
||||
for a detailed structure of this object.
|
||||
|
||||
### getCurrentAppiumContext
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
|
||||
### setAppiumContext
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------ | -------------------------------------------- | ------ |
|
||||
| `name` | Name of the context to set as the active one | string |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumContexts
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/contexts
|
||||
```
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
### getLogEvents
|
||||
|
||||
```
|
||||
@@ -393,3 +435,71 @@ Retrieves data from a directory on the device under test.
|
||||
#### Response
|
||||
|
||||
`string` - the Base64-encoded zip file of the directory contents
|
||||
|
||||
### getAppiumRotation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
|
||||
| Name | Description | Type |
|
||||
| ---- | ---------------------------------------------------- | ------ |
|
||||
| `x` | Degrees by which the device is rotated on its X axis | number |
|
||||
| `y` | Degrees by which the device is rotated on its Y axis | number |
|
||||
| `z` | Degrees by which the device is rotated on its Z axis | number |
|
||||
|
||||
### setAppiumRotation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ---- | ---------------------------------------------------- | ------ |
|
||||
| `x` | Degrees by which the device is rotated on its X axis | number |
|
||||
| `y` | Degrees by which the device is rotated on its Y axis | number |
|
||||
| `z` | Degrees by which the device is rotated on its Z axis | number |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumOrientation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
|
||||
### setAppiumOrientation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| `orientation` | New device orientation. Supported values are `PORTRAIT` or `LANDSCAPE`. | string |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
@@ -140,6 +140,10 @@ GET /session/:sessionId/orientation
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumOrientation`](./appium.md#getappiumorientation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
@@ -154,6 +158,10 @@ POST /session/:sessionId/orientation
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumOrientation`](./appium.md#setappiumorientation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
|
||||
@@ -21,6 +21,10 @@ GET /session/:sessionId/rotation
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumRotation`](./appium.md#getappiumrotation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
@@ -41,6 +45,10 @@ POST /session/:sessionId/rotation
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumRotation`](./appium.md#setappiumrotation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
@@ -63,6 +71,10 @@ GET /session/:sessionId/context
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getCurrentAppiumContext`](./appium.md#getcurrentappiumcontext) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
@@ -77,6 +89,10 @@ POST /session/:sessionId/context
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumContext`](./appium.md#setappiumcontext) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
@@ -97,6 +113,10 @@ GET /session/:sessionId/contexts
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumContexts`](./appium.md#getappiumcontexts) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
@@ -170,10 +170,17 @@ Modifies the [`createSession`](./webdriver.md#createsession) endpoint:
|
||||
All endpoints for this plugin can be invoked without creating a session, allowing you to prepare
|
||||
your test environment in advance.
|
||||
|
||||
!!! info
|
||||
|
||||
Prior to plugin version 1.2.0, all endpoints below were mounted under the `/storage` prefix
|
||||
(e.g. `/storage/add`) instead of `/appium/storage`. The `/storage`-prefixed routes are still
|
||||
available for backward compatibility, but are deprecated and will be removed in a future
|
||||
version of the plugin.
|
||||
|
||||
### addStorageItem
|
||||
|
||||
```
|
||||
POST /storage/add
|
||||
POST /appium/storage/add
|
||||
```
|
||||
|
||||
Adds a new file to the storage.
|
||||
@@ -200,8 +207,8 @@ Example:
|
||||
```json
|
||||
{
|
||||
"ws": {
|
||||
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
"stream": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
},
|
||||
"ttlMs": 300000
|
||||
}
|
||||
@@ -210,7 +217,7 @@ Example:
|
||||
### deleteStorageItem
|
||||
|
||||
```
|
||||
POST /storage/delete
|
||||
POST /appium/storage/delete
|
||||
```
|
||||
|
||||
Deletes a file in the storage.
|
||||
@@ -229,7 +236,7 @@ the storage, or if the delete request is invalid.
|
||||
### listStorageItems
|
||||
|
||||
```
|
||||
GET /storage/list
|
||||
GET /appium/storage/list
|
||||
```
|
||||
|
||||
List all files present in the storage.
|
||||
@@ -247,7 +254,7 @@ List all files present in the storage.
|
||||
### resetStorage
|
||||
|
||||
```
|
||||
POST /storage/reset
|
||||
POST /appium/storage/reset
|
||||
```
|
||||
|
||||
Deletes all uploaded files and stops any incomplete uploads.
|
||||
|
||||
@@ -159,14 +159,14 @@ extensions include scripts.
|
||||
#### Usage
|
||||
|
||||
```
|
||||
appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
| ---------------- | --------------------------------------------- |
|
||||
| `extension-name` | The short name of the installed extension |
|
||||
| `script-name` | The name of the script to be run |
|
||||
| `script-args` | Any additional arguments passed to the script |
|
||||
| Argument | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `extension-name` | The short name of the installed extension |
|
||||
| `script-name` | The name of the script to run. If not provided, a list of available scripts is returned. |
|
||||
| `script-args` | Any additional arguments passed to the script |
|
||||
|
||||
#### Options
|
||||
|
||||
@@ -182,6 +182,12 @@ appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium driver run uiautomator2 reset
|
||||
```
|
||||
|
||||
- List all available scripts included in the XCUITest driver:
|
||||
|
||||
```
|
||||
appium driver run xcuitest
|
||||
```
|
||||
|
||||
## `update`
|
||||
|
||||
Updates one or more extensions. Only supported for extensions installed via `npm`. By default,
|
||||
|
||||
@@ -29,6 +29,7 @@ appium
|
||||
| `--address`, `-a` | IPv4/IPv6 address to listen on | string | `0.0.0.0` |
|
||||
| `--allow-cors` | Allow web browser connections from any host | boolean | `false` |
|
||||
| `--allow-insecure` | List of [insecure features](../../guides/security.md) that should be allowed in this server's sessions. Individual features can be overridden by `--deny-insecure`. Has no effect in combination with `--relaxed-security`. | array<string> | `[]` |
|
||||
| `--allow-unknown-args` | Do not exit if unrecognized command-line arguments are passed to the server; ignore them instead. Useful when the Appium CLI is wrapped by external tooling that appends extra flags. | boolean | `false` |
|
||||
| `--base-path`, `-pa` | Base path to use as the prefix for all webdriver routes running on the server | string | `""` |
|
||||
| `--callback-address`, `-ca` | Callback IP address | string | `0.0.0.0` |
|
||||
| `--callback-port`, `-cp` | Callback port | integer | `4723` |
|
||||
@@ -64,7 +65,7 @@ appium
|
||||
| `--tmp` | Absolute path to the directory used for temporary files | string | [`os.tmpdir()`](https://nodejs.org/api/os.html#ostmpdir) |
|
||||
| `--use-drivers` | List of drivers to activate. By default, all installed drivers are activated. | array<string> | `[]` |
|
||||
| `--use-plugins` | List of plugins to activate. By default, no plugins are activated. Set to `["all"]` to activate all installed plugins. | array<string> | `[]` |
|
||||
| `--webhook`, `-G` | URL for an HTTP listener where the server logs should be output. This does not affect output on the console. | string | |
|
||||
| `--webhook`, `-G` | URL for an HTTP listener where the server logs should be output. This does not affect output on the console. A bare `host:port` is also accepted, which posts the logs to the root path over plain http; a value having neither a scheme nor a port falls back to `127.0.0.1:9003`. | string | |
|
||||
|
||||
### Info Options
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ title: Additional Resources
|
||||
|
||||
Here you can find links to additional Appium resources around the web:
|
||||
|
||||
## Websites
|
||||
|
||||
- [Appium Pro](https://appiumpro.com) - a blog and newsletter written by one of Appium's
|
||||
maintainers, Jonathan Lipps, with lots of useful guides
|
||||
|
||||
## Online Courses
|
||||
|
||||
- [Appium and Selenium Fundamentals](https://ui.headspin.io/university/learn/appium-selenium-fundamentals-2020) - a comprehensive video course on learning Python, Selenium, and Appium by Jonathan Lipps
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||
/* eslint-disable promise/prefer-await-to-then */
|
||||
|
||||
const {deploy, buildSite} = require('@appium/docutils');
|
||||
const {
|
||||
log,
|
||||
LANGS,
|
||||
@@ -35,6 +34,9 @@ const push = Boolean(process.env.APPIUM_DOCS_PUBLISH);
|
||||
* Builds and optionally deploys docs for each supported language.
|
||||
*/
|
||||
async function main() {
|
||||
// @appium/docutils is ESM-only; this file is CommonJS, so it must be loaded dynamically
|
||||
const {deploy, buildSite} = await import('@appium/docutils');
|
||||
|
||||
log.info(`Building Appium docs and committing to ${DOCS_BRANCH}`);
|
||||
|
||||
const semVersion = semver.parse(version);
|
||||
|
||||
@@ -5,9 +5,9 @@ authors:
|
||||
date: 2024-07-13
|
||||
---
|
||||
|
||||
# Appium Conf 2024: Call for Proposals Now Open!
|
||||
# Appium Conf 2024: 现在开始征集提案
|
||||
|
||||
We're thrilled to announce that Appium Conf is back and going online for 2024! This is the official Appium conference where you can learn all about what's new in Appium 2.0 and explore the latest in automating tests for devices, mobile apps, and desktop applications.
|
||||
我们很兴奋地宣布,Appium Conf 已回归并在2024年上线! 这是官方的Appium会议,您可以在会上了解Appium 2中的所有新内容。 并探索设备自动化测试、移动应用程序和桌面应用程序的最新版本。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
@@ -17,29 +17,29 @@ We're thrilled to announce that Appium Conf is back and going online for 2024! T
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Mark Your Calendars: September 13, 2024
|
||||
## 标记您的日历:2024年9月13日
|
||||
|
||||
Join us on September 13, 2024, for a full day of inspiring and practical talks from the Appium team and the world's top mobile testing and automation experts. With Appium 2.0 now well-established, this is your opportunity to dive deep and learn how to maximize its potential.
|
||||
2024年9月13日,请加入我们,参加Appium团队和世界顶级移动测试和自动化专家为期一天的鼓舞人心且实用的演讲。 随着Appium 2.0的成熟,这是您深入了解并学习如何最大限度地发挥其潜力的机会。
|
||||
|
||||
## Why Attend Appium Conf 2024?
|
||||
## 为什么参加了2024年的Appium Conf ?
|
||||
|
||||
1. **Gain Insights**: Hear interesting insights and inspiring case studies from global experts and the core Appium team.
|
||||
2. **Improve Your Strategy**: Gain valuable knowledge to enhance your own automation strategy.
|
||||
3. **Network**: Make connections with Appium users from around the globe.
|
||||
4. **Learn from Specialists**: Benefit from Appium specialists' expertise on a variety of topics.
|
||||
1. **获得见解**:听取全球专家和核心Appium团队的有趣见解和鼓舞人心的案例研究。
|
||||
2. **改进您的战略**:获得有价值的知识,以增强您自己的自动化战略。
|
||||
3. **网络**:与来自全球的Appium用户建立联系。
|
||||
4. **向专家学习**:从Appium专家在各种主题上的专业知识中受益。
|
||||
|
||||
## Call for Proposals: Share Your Knowledge!
|
||||
## 征集提案:分享您的知识!
|
||||
|
||||
We're looking for a diverse range of speakers from around the globe who are eager to share innovative ideas and use-cases of mobile test automation. If you have experience with Appium and insights that could benefit the community of test practitioners, we want to hear from you!
|
||||
我们正在寻找来自全球各地的各种演讲者,他们渴望分享移动测试自动化的创新想法和用例。 如果您有使用Appium的经验和见解,可以使测试从业者社区受益,我们想听听您的意见!
|
||||
|
||||
Don't miss this chance to contribute to the Appium community and showcase your expertise on a global stage.
|
||||
不要错过这个机会,为Appium社区做出贡献,并在全球舞台上展示你的专业知识。
|
||||
|
||||
## How to Submit Your Proposal
|
||||
## 如何提交您的提案
|
||||
|
||||
Visit our website at [https://appiumconf.com/](https://appiumconf.com/) and [Submit a Proposal](https://confengine.com/conferences/appium-conf-2024/proposals?utm_medium=referal&utm_source=appium.io&utm_campaign=launch). We're excited to see your innovative ideas and experiences with Appium!
|
||||
访问我们的网站 [https://appiumconf.com/](https://appiumconf.com/) 并[提交提案](https://confengine.com/conferences/appium-conf-2024/proposals?utm_medium=referal&utm_source=appium.io&utm_campaign=launch)。 我们很高兴看到您在 Appium 中的创新想法和经验!
|
||||
|
||||
## Stay Tuned
|
||||
## 敬请期待
|
||||
|
||||
Keep an eye on our website and social media channels for updates on the conference schedule, keynote speakers, and more exciting details about Appium Conf 2024.
|
||||
关注我们的网站和社交媒体频道以了解会议日程安排的更新, 主旨演讲人和2024年Appium Conf 的更令人兴奋的细节。
|
||||
|
||||
Join us in shaping the future of mobile and device test automation. Submit your proposal today and be part of this exciting event!
|
||||
加入我们来塑造移动设备测试自动化的未来。 立即提交您的提案,成为这一激动人心的活动的一部分!
|
||||
@@ -8,11 +8,13 @@ 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 -->
|
||||
|
||||
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.
|
||||
|
||||
<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" />
|
||||
@@ -20,15 +22,19 @@ This kind of hands-on contribution is crucial for ensuring Appium continues to e
|
||||
</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.
|
||||
|
||||
- [`appium-mcp`](https://github.com/appium/appium-mcp) – Supporting Appium's implementation of the Model Context Protocol (MCP) to drive forward modern AI-assisted capabilities.
|
||||
- [`appium-ios-remotexpc`](https://github.com/appium/appium-ios-remotexpc) – Contributing to the native macOS/XPC-based remote execution layer, essential for iOS automation.
|
||||
- [`appium-ios-tuntap`](https://github.com/appium/appium-ios-tuntap) – 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
|
||||
|
||||
**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!
|
||||
|
||||
@@ -1,84 +1,66 @@
|
||||
---
|
||||
title: Building Documentation
|
||||
title: 构建文档
|
||||
---
|
||||
|
||||
Once you've [built a driver](./build-drivers.md) or [built a plugin](./build-plugins.md) for Appium,
|
||||
you will hopefully want to document how that extension works for your users. The most basic way of
|
||||
doing this is to write up a quick `README.md` and keep it in the root of your project's repository.
|
||||
However, this can involve a lot of effort.
|
||||
一旦你为 Appium 构建了 [驱动](./build-drivers.md) 或 [插件](./build-plugins.md),通常也会希望为用户说明这个扩展是如何工作的。 最基础的方式就是编写一个简短的 `README.md`,并将它放在项目仓库的根目录中。
|
||||
不过,这样做可能会耗费不少精力。
|
||||
|
||||
The Appium project has built tools to help with this, and we've packaged up these tools so our
|
||||
ecosystem developers building drivers and plugins can _also_ use them. The best way to get going
|
||||
with these tools is probably to look at an existing Appium driver repo to see how it's done, for
|
||||
example the [XCUITest driver repo](https://github.com/appium/appium-xcuitest-driver). But this guide
|
||||
will outline the basic approach.
|
||||
Appium 项目已经提供了辅助工具,并且我们把这些工具打包好了,以便生态系统中的驱动和插件开发者也能使用。 这些工具最好的入门方式是查看一个现有的 Appium 驱动仓库,看看它是如何组织文档的,例如 [XCUITest 驱动仓库](https://github.com/appium/appium-xcuitest-driver)。 不过,本指南会先介绍基本的实现思路。
|
||||
|
||||
### Conceptual architecture
|
||||
### 概念架构
|
||||
|
||||
Appium settled on [MkDocs](https://www.mkdocs.org/) as a Markdown-based documentation site
|
||||
generator. It uses a Python toolchain (and not Node.js), but it turned out to be the best option
|
||||
for our purposes. You can adjust this, but by default Appium's utilities also assume that you'll be
|
||||
using the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) theme/extension for
|
||||
MkDocs.
|
||||
Appium 采用 [MkDocs](https://www.mkdocs.org/) 作为基于 Markdown 的文档站点的生成工具。 它使用 Python 工具链(而不是 Node.js),但这恰好是我们当前需求下最合适的选择。 你可以自行调整,不过默认情况下,Appium 的工具链也会假设你使用 [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) 主题与扩展来搭建 MkDocs。
|
||||
|
||||
In order to make different versions of your docs available (one for each minor release of your
|
||||
extension, typically), we also bundle [Mike](https://github.com/jimporter/mike).
|
||||
为了让你的文档能够支持不同版本(通常是每个小版本一个),我们还会捆绑 [Mike](https://github.com/jimporter/mike)。
|
||||
|
||||
From here, building a basic docs site is as easy as collecting your Markdown files together and
|
||||
defining how you want them to be organized.
|
||||
从这里开始,构建一个基础文档站点其实很简单:把 Markdown 文件收集起来,再定义你希望它们如何组织即可。
|
||||
|
||||
### Prerequisites
|
||||
### 前置条件
|
||||
|
||||
To take advantage of Appium's documentation utilities, you'll need to install:
|
||||
要使用 Appium 的文档工具,你需要安装以下内容:
|
||||
|
||||
- [Python v3+](https://www.python.org/downloads/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installation/) (this may be installed automatically with Python)
|
||||
- The `@appium/docutils` package:
|
||||
- [pip](https://pip.pypa.io/en/stable/installation/)(通常 Python 会自动附带安装)
|
||||
- `@appium/docutils` 包:
|
||||
|
||||
```bash
|
||||
npm install --save-dev @appium/docutils
|
||||
```
|
||||
|
||||
### Initializing an Extension for Building Docs
|
||||
### 初始化用于构建文档的扩展
|
||||
|
||||
To prepare your extension for generating documentation, run the following command:
|
||||
要为你的扩展准备文档生成环境,请运行下面的命令:
|
||||
|
||||
```bash
|
||||
npx appium-docs init
|
||||
```
|
||||
|
||||
This will:
|
||||
这一步会:
|
||||
|
||||
1. Create a `tsconfig.json` if one does not already exist. This is necessary even if your extension
|
||||
is not written in TypeScript.
|
||||
2. Create a `mkdocs.yml` with the necessary configuration for MkDocs.
|
||||
1. 创建一个 `tsconfig.json`(如果当前还没有的话)。 即使你的扩展并不是 TypeScript 编写的,这也是必需的。
|
||||
2. 创建一个 `mkdocs.yml`,其中包含 MkDocs 所需的配置。
|
||||
|
||||
### Documenting Your Extension
|
||||
### 为你的扩展编写文档
|
||||
|
||||
At this point, you can begin documenting your extension. By default, MkDocs will look for Markdown
|
||||
files in the `docs` directory. You can therefore create your Markdown documentation files, place
|
||||
them in `docs`, and add links to these files in `mkdocs.yml`.
|
||||
此时你就可以开始为扩展编写文档了。 默认情况下,MkDocs 会在 `docs` 目录中查找 Markdown 文件。 因此,你可以创建文档文件并放入 `docs`,然后在 `mkdocs.yml` 里添加相应链接。
|
||||
|
||||
Refer to the [MkDocs documentation](https://www.mkdocs.org/user-guide/writing-your-docs/) for
|
||||
information on how to organize and structure your documentation.
|
||||
请参考 [MkDocs 文档](https://www.mkdocs.org/user-guide/writing-your-docs/) 来了解如何组织与构建你的文档结构。
|
||||
|
||||
### Building the Docs
|
||||
### 构建文档
|
||||
|
||||
At this point, you can use the `appium-docs` CLI tool. Run this tool with no arguments to get the
|
||||
full help output and see all the available subcommands and parameters. Here are a few usage
|
||||
examples:
|
||||
在这个阶段,你可以使用 `appium-docs` CLI 工具。 运行这个工具时不传任何参数,可以看到完整的帮助信息,以及所有可用的子命令与参数。 下面是几个常见用法示例:
|
||||
|
||||
```bash
|
||||
# Generate reference and build the mkdocs site into the site dir
|
||||
# 生成参考文档并将 mkdocs 站点构建到 site 目录
|
||||
npx appium-docs build
|
||||
|
||||
# Same as build, but host the docs on a local dev server
|
||||
# and watch for changes and rebuild when files change
|
||||
# 与 build 相同,但会在本地启动开发服务器
|
||||
# 并监听文件变化,在内容变更时自动重建
|
||||
npx appium-docs build --serve
|
||||
|
||||
# Build the docs and deploy them with mike versioning to the docs-site branch
|
||||
# using the included commit message.
|
||||
# This is particularly useful for pushing content to a GitHub pages branch!
|
||||
# 构建文档并使用 mike 进行版本化部署到 docs-site 分支
|
||||
# 其中包含内置提交信息。
|
||||
# 这对于把内容推送到 GitHub Pages 分支特别有用!
|
||||
npx appium-docs build \
|
||||
--deploy \
|
||||
-b docs-site \
|
||||
|
||||
@@ -1,45 +1,33 @@
|
||||
---
|
||||
title: Building Doctor Checks
|
||||
title: 构建 Doctor Checks
|
||||
---
|
||||
|
||||
The idea of Appium Doctor is to assist users with driver or plugin preconditions setup. Sometimes such
|
||||
preconditions might be quite complicated and require non-trivial technical knowledge. Doctor checks,
|
||||
which are vanilla Node.js class instances written by extension authors, simplify
|
||||
the setup process by automating diagnostics and possible fixes for the found issues. These checks
|
||||
might also be interactive to ensure better usage experience.
|
||||
Appium Doctor 的目标,是帮助用户完成驱动或插件的前置条件配置。 有时这些前置条件可能相当复杂,需要一定的技术知识。 Doctor checks是由扩展作者编写的普通 Node.js 类实例,它可以自动完成诊断并尝试修复发现的问题。 为了提供更好的使用体验,这些检查项有时也会是交互式的。
|
||||
|
||||
This tutorial is supposed to be used by plugin or driver authors that would like to help their users
|
||||
to deal with complicated setup or configuration steps.
|
||||
本教程适用于希望帮助用户处理复杂配置或设置步骤的插件或驱动作者。
|
||||
|
||||
## Adding Doctor Checks
|
||||
## 添加 Doctor Checks
|
||||
|
||||
### Typing Requirements
|
||||
### 类型要求
|
||||
|
||||
The term `Doctor Check` literally describes a single javascript class instance that implements the
|
||||
[IDoctorCheck interface](https://github.com/appium/appium/blob/master/packages/types/lib/doctor.ts).
|
||||
The interface defines the following methods and properties:
|
||||
术语 `Doctor Check` 的字面含义,是指一个实现了 [IDoctorCheck 接口](https://github.com/appium/appium/blob/master/packages/types/lib/doctor.ts) 的 JavaScript 类实例。
|
||||
这个接口定义了以下方法和属性:
|
||||
|
||||
- `diagnose(): Promise<DoctorCheckResult>`: Contains the code to diagnose a possible issue
|
||||
- `fix(): Promise<string|null>`: Either fixes the actual problem if `hasAutofix()` returns true or
|
||||
returns a string description for possible manual fixes. If this method throws an exception named
|
||||
`FixSkippedError` and `hasAutofix()` returns true then the result of the method invocation
|
||||
is going to be ignored.
|
||||
- `hasAutofix(): boolean`: Whether calling `fix()` would resolve the found issue
|
||||
- `isOptional(): boolean`: Whether the found issue can be ignored and is not a showstopper
|
||||
- `log: AppiumLogger`: May be used for logging. This property may be assigned
|
||||
by the instance itself or by the Appium server if it is left unassigned.
|
||||
- `diagnose(): Promise<DoctorCheckResult>`:包含诊断某个问题的代码
|
||||
- `fix(): Promise<string|null>`:如果 `hasAutofix()` 返回 true,则修复实际问题;否则返回一段手动修复的说明。 如果此方法抛出名为 `FixSkippedError` 的异常,并且 `hasAutofix()` 返回 true,那么该调用结果将被忽略。
|
||||
- `hasAutofix(): boolean`:表示调用 `fix()` 是否能解决发现的问题
|
||||
- `isOptional(): boolean`:表示发现的问题是否可以被忽略且不是阻塞性问题
|
||||
- `log: AppiumLogger`:可用于日志输出。 这个属性可以由实例自行赋值,也可以由 Appium 服务在未赋值时自动提供。
|
||||
|
||||
The `DoctorCheckResult` object returned by the `diagnose()` method must contain the following properties:
|
||||
`diagnose()` 方法返回的 `DoctorCheckResult` 对象必须包含以下属性:
|
||||
|
||||
- `ok: boolean`: Whether the diagnosis found no issues
|
||||
- `optional: boolean`: Whether the diagnosed issue is safe to ignore
|
||||
- `message: string`: The text message describing the diagnostic result
|
||||
- `ok: boolean`:诊断是否未发现任何问题。
|
||||
- `optional: boolean`:诊断出的问题是否可以安全忽略。
|
||||
- `message: string`:描述诊断结果的文本信息。
|
||||
|
||||
### Manifest Requirements
|
||||
### 清单要求
|
||||
|
||||
A single extension may export multiple Doctor checks to Appium. In order for these checks to be properly
|
||||
picked up by the server CLI after the corresponding extension is installed they might be listed in the
|
||||
package .json manifest under the `appium.doctor.checks` section similar to the definition below:
|
||||
一个扩展可以向 Appium 导出多个 Doctor checks。 为了让这些检查项在对应扩展安装后能够被服务端 CLI 正确识别,它们需要在package .json 清单文件中列在 `appium.doctor.checks` 部分,格式类似下面这样:
|
||||
|
||||
```json
|
||||
// ...
|
||||
@@ -67,12 +55,11 @@ package .json manifest under the `appium.doctor.checks` section similar to the d
|
||||
// ...
|
||||
```
|
||||
|
||||
Also, it makes sense to include the [@appium/types](https://www.npmjs.com/package/@appium/types) import
|
||||
to the package dev dependencies.
|
||||
此外,建议将 [@appium/types](https://www.npmjs.com/package/@appium/types) 加入包的开发依赖中。
|
||||
|
||||
### Implementation Example
|
||||
### 实现示例
|
||||
|
||||
The below example is a "raw" Node.JS implementation that does not use any transpilation:
|
||||
下面是一个“原生”的 Node.js 实现示例,没有使用任何转译工具:
|
||||
|
||||
```js
|
||||
const {fs, doctor} = require('@appium/support');
|
||||
@@ -123,8 +110,7 @@ module.exports = {androidHomeCheck};
|
||||
*/
|
||||
```
|
||||
|
||||
This file could be saved as `doctor/android-home-check.js` and then added to the package.json manifest
|
||||
as
|
||||
这个文件可以保存为 `doctor/android-home-check.js`,然后再将其加入到 package.json 清单中,例如:
|
||||
|
||||
```json
|
||||
// ...
|
||||
|
||||
@@ -222,17 +222,24 @@ So you'll probably end up overriding `createSession`. You can do so by defining
|
||||
driver:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData) {
|
||||
const [sessionId, caps] = super.createSession(w3cCaps);
|
||||
async createSession(w3cCaps) {
|
||||
const [sessionId, caps] = await super.createSession(w3cCaps);
|
||||
// do your own stuff here
|
||||
return [sessionId, caps];
|
||||
}
|
||||
```
|
||||
|
||||
For legacy reasons, your function will receive old-style JSON Wire Protocol desired and required
|
||||
caps as the first two arguments. Given that the old protocol isn't supported anymore and clients
|
||||
have all been updated, you can instead only rely on the `w3cCaps` parameter. (For a discussion
|
||||
about what `otherDriverData` is about, see the section below on concurrent drivers).
|
||||
!!! warning "Deprecated"
|
||||
|
||||
```
|
||||
Older drivers may still define `createSession` with up to four parameters, e.g.
|
||||
`createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData)`. This shape is a holdover from the
|
||||
retired JSON Wire Protocol, which required desired and required caps as the first two
|
||||
arguments; only the first W3C-shaped argument was ever used. The `otherDriverData` parameter is
|
||||
likewise deprecated in favor of [IPC](#send-messages-to-plugins-running-on-the-same-session) for
|
||||
coordinating with other concurrently-running sessions. New drivers should use the single-argument
|
||||
form shown above.
|
||||
```
|
||||
|
||||
You'll want to make sure to call `super.createSession` in order to get the session ID as well as
|
||||
the processed capabilities (note that capabilities are also set on `this.caps`; modifying `caps`
|
||||
@@ -522,14 +529,13 @@ route, then the route will not be proxied and instead will be handled by your dr
|
||||
example, we are avoiding proxying all `POST` routes that have the `appium` prefix.
|
||||
|
||||
Next, we have to set up the proxying itself. The way to do this is to use a special class from
|
||||
Appium called `JWProxy`. (The name means "JSON Wire Proxy" and is related to a legacy
|
||||
implementation of the protocol). You'll want to create a `JWProxy` object using the details required to
|
||||
connect to the remote server:
|
||||
Appium called `WebDriverProxy`. You'll want to create a `WebDriverProxy` object using the details
|
||||
required to connect to the remote server:
|
||||
|
||||
```js
|
||||
// import {JWProxy} from 'appium/driver';
|
||||
// import {WebDriverProxy} from 'appium/driver';
|
||||
|
||||
const proxy = new JWProxy({
|
||||
const proxy = new WebDriverProxy({
|
||||
server: 'remote.server',
|
||||
port: 1234,
|
||||
base: '/',
|
||||
@@ -541,8 +547,8 @@ this.proxyCommand = proxy.command.bind(proxy);
|
||||
|
||||
Here we are creating a proxy object and assigning some of its methods to `this` under the names
|
||||
`proxyReqRes` and `proxyCommand`. This is required for Appium to use the proxy, so don't forget
|
||||
this step! The `JWProxy` has a variety of other options which you can check out in the source code,
|
||||
as well. (TODO: publish options as API docs and link here).
|
||||
this step! The `WebDriverProxy` has a variety of other options which you can check out in the
|
||||
source code as well.
|
||||
|
||||
Finally, we need a way to tell Appium when the proxy is active. For your driver it might always
|
||||
be active, or it might only be active when in a certain context. You can define the logic as an
|
||||
@@ -799,6 +805,14 @@ that multiple simultaneous sessions don't use the same resources:
|
||||
3. Have each driver express what resources it is using, then examine currently-used resources from
|
||||
other drivers when a new session begins.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
```
|
||||
The `driverData`-based mechanism described below is deprecated. Prefer
|
||||
[IPC](#send-messages-to-plugins-running-on-the-same-session) for coordinating resources across
|
||||
concurrently-running sessions instead.
|
||||
```
|
||||
|
||||
To support this third strategy, you can implement `get driverData` in your driver to return what
|
||||
sorts of resources your driver is currently using, for example:
|
||||
|
||||
@@ -810,7 +824,7 @@ get driverData() {
|
||||
|
||||
Now, when a new session is started on your driver, the `driverData` response from any other
|
||||
simultaneously running drivers (of the same type) will also be included, as the last parameter of
|
||||
the `createSession` method:
|
||||
the deprecated, multi-argument form of the `createSession` method:
|
||||
|
||||
```js
|
||||
async createSession(jwpCaps, reqCaps, w3cCaps, driverData)
|
||||
|
||||
@@ -483,9 +483,9 @@ capability.
|
||||
You probably don't normally need to update the Appium server object (which is an
|
||||
[Express](https://expressjs.com/) server having already been
|
||||
[configured](https://github.com/appium/appium/blob/master/packages/base-driver/lib/express/server.js)
|
||||
in a variety of ways). But, for example, you could add new Express middleware to the server to
|
||||
support your plugin's requirements. To update the server you must implement the `static async
|
||||
updateServer` method in your class. This method takes three parameters:
|
||||
in a variety of ways). But, for example, you could add new routes to the server to support your
|
||||
plugin's requirements. To update the server you must implement the `static async updateServer`
|
||||
method in your class. This method takes three parameters:
|
||||
|
||||
- `expressApp`: the Express app object
|
||||
- `httpServer`: the Node HTTP server object
|
||||
@@ -498,6 +498,25 @@ results you'll need to test! Warning: this should be considered an advanced feat
|
||||
knowledge of Express, as well as the care not to do anything that could affect the operation of
|
||||
other parts of the Appium server!
|
||||
|
||||
`updateServer` runs _after_ Appium's own routes are registered, so `expressApp.use(...)` here
|
||||
won't see requests to paths Appium already owns — Express matches middleware and routes in
|
||||
registration order. For middleware that must see requests to those routes too (e.g. logging or
|
||||
auth), use `httpServer.frontRouter` instead: Appium mounts this
|
||||
[Router](https://expressjs.com/en/5x/api/router/) before any route is registered, so anything
|
||||
attached to it — even from inside `updateServer` — still runs ahead of route matching. Note this
|
||||
is still _after_ Appium's own baseline middleware (CORS, WebSocket upgrade handling, body
|
||||
parsing, etc.), so it won't see requests Appium itself has already rejected, e.g. a CORS
|
||||
preflight failure or a body that failed to parse:
|
||||
|
||||
```js
|
||||
static async updateServer(expressApp, httpServer, cliArgs) {
|
||||
httpServer.frontRouter.use((req, res, next) => {
|
||||
console.log(`Incoming request: ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Handle unexpected session shutdown
|
||||
|
||||
When developing a plugin you may want to add some cleanup logic for when a session ends. You would
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
hide:
|
||||
- toc
|
||||
|
||||
title: Developing Appium Extensions
|
||||
title: 开发 Appium 扩展
|
||||
---
|
||||
|
||||
Appium is built with a modular structure, which means that Appium extensions (drivers and plugins)
|
||||
are decoupled from the main Appium module, and you only need to install the extensions that you
|
||||
want to use. This modular structure also unlocks the ability to develop entirely new extensions!
|
||||
Appium 采用模块化结构,这意味着 Appium 扩展(驱动和插件)与主 Appium 模块是解耦的;你只需要安装自己想要使用的扩展即可。 这种模块化结构还使得开发全新的扩展变得成为可能。
|
||||
|
||||
This section of the Appium documentation is intended to help aspiring developers with creating their
|
||||
own Appium extension:
|
||||
Appium 文档的这一部分,旨在帮助有志于开发者创建自己的 Appium 扩展:
|
||||
|
||||
- For creating a driver, see the [Build Drivers](./build-drivers.md) page
|
||||
- For creating a plugin, take a look at the [Build Plugins](build-plugins.md) page
|
||||
- Drivers and plugins both need documentation, so check out the [Build Documentation](./build-docs.md) page
|
||||
- For creating a doctor check, see the [Building Doctor Checks](./build-doctor-checks.md) page
|
||||
- 想要创建驱动,请查看 [构建驱动](./build-drivers.md) 页面
|
||||
- 想要创建插件,请查看 [构建插件](build-plugins.md) 页面
|
||||
- 驱动和插件都需要文档,因此请查看 [构建文档](./build-docs.md) 页面
|
||||
- 想要创建 Doctor check,请查看 [构建Doctor Checks](./build-doctor-checks.md) 页面
|
||||
|
||||
@@ -2,48 +2,38 @@
|
||||
hide:
|
||||
- toc
|
||||
|
||||
title: Masking Sensitive Log Data
|
||||
title: 屏蔽敏感日志数据
|
||||
---
|
||||
|
||||
Since Appium server version 2.18.0 there is a possibility to mask sensitive
|
||||
values in logs. The below tutorial explains how to use this feature in third-party
|
||||
extensions.
|
||||
自 Appium server 2.18.0 版本起,已经具备了在日志中屏蔽敏感值的能力。 下面的教程将介绍如何在第三方扩展中使用这一功能。
|
||||
|
||||
## Why It Might Be Useful
|
||||
## 为什么这很有用
|
||||
|
||||
It is the right call to hide sensitive information, like passwords, tokens, etc.
|
||||
from server logs, so it does not accidentally leak if these logs end up in wrong hands.
|
||||
Appium server already provides a way to manipulate logs records via
|
||||
[filtering](../guides/log-filters.md), although it has its own limitations.
|
||||
The current approach is more sophisticated though, and requires some fine-tuning
|
||||
on the driver/plugin side.
|
||||
将密码、令牌等敏感信息从服务器日志中隐藏起来是合理且必要的,这样即使日志落入错误的人手中,也不会意外泄露敏感数据。
|
||||
Appium server 已经提供了一种通过 [过滤](../guides/log-filters.md) 来处理日志记录的方式,不过它本身存在一些限制。
|
||||
当前这种方法则更为先进,但需要驱动/插件侧进行一些细调。
|
||||
|
||||
## How To
|
||||
## 使用方法
|
||||
|
||||
The assumption is that your extension uses the standard built-in
|
||||
[@appium/logger](https://www.npmjs.com/package/@appium/logger).
|
||||
In order to get some value in logs replaced by a generic mask it is necessary:
|
||||
假设你的扩展使用的是标准内置的 [@appium/logger](https://www.npmjs.com/package/@appium/logger)。
|
||||
要让日志中的某个值被替换成通用的掩码,需要执行以下步骤:
|
||||
|
||||
- Change the logging expression to wrap sensitive values and format them, for example:
|
||||
- 将日志表达式改为包裹敏感值并进行格式化,例如:
|
||||
|
||||
```js
|
||||
this.log.info(`Value: ${value}`);
|
||||
this.log.info(`Value: ${value}`);
|
||||
```
|
||||
|
||||
becomes
|
||||
变为
|
||||
|
||||
```js
|
||||
import {logger} from '@appium/support';
|
||||
|
||||
this.log.info('Value: %s', logger.markSensitive(value));
|
||||
this.log.info('Value: %s', logger.markSensitive(value));
|
||||
```
|
||||
|
||||
The formatting happens via the standard Node.js's
|
||||
[util.format](https://nodejs.org/api/util.html#utilformatformat-args) API.
|
||||
格式化过程会通过标准的 Node.js [util.format](https://nodejs.org/api/util.html#utilformatformat-args) API 完成。
|
||||
|
||||
- While sending the appropriate server request, where this log line is used and should be masked,
|
||||
add the custom header `X-Appium-Is-Sensitive` with its value set to `1` or `true` (case-insensitive).
|
||||
Without such header the above log value is not going to be masked.
|
||||
This way it is possible to conditionally mask log records depending on which
|
||||
request is being handled by the extension if the log expression is used in the
|
||||
common section.
|
||||
- 在发送相应的服务器请求时,如果这个日志行被使用并且应该被掩码处理,请额外添加自定义请求头 `X-Appium-Is-Sensitive`,其值为 `1` 或 `true`(大小写不敏感)。
|
||||
如果没有这个请求头,上述日志值将不会被掩码处理。
|
||||
这样就可以根据扩展处理的具体请求来有条件地屏蔽日志记录;如果日志表达式位于公共部分,就可以在这里统一控制。
|
||||
|
||||
@@ -23,7 +23,7 @@ title: Appium 如何工作?
|
||||
|
||||
这里还有另一个更大的问题,鉴于除了 iOS 和 Android 之外还有更多应用平台:
|
||||
|
||||
- 我们如何为_所有_平台启用自动化?
|
||||
- 我们如何为_所有_平台启用自动化?
|
||||
|
||||
探索 Appium 对这些问题的答案可能不是学习 Appium 是什么的最快方式,但它肯定是一个好方法! 所以让我们深入了解。
|
||||
|
||||
@@ -38,12 +38,12 @@ Appium 的最初目标是为移动应用(iOS 和 Android)开发自动化标
|
||||
|
||||
[^2]: 从技术上讲,当 Appium 首次编写时,我们处理的是比 WebDriver 规范更古老的东西,称为 JSON Wire Protocol。 从那时起,Appium 继续与 W3C 规范一起演化,并完全符合 W3C 规范。
|
||||
|
||||
当然,Appium 希望支持用户交互从 Web 到移动或 Web 到电视确实不同的情况,因此 Appium 还利用 WebDriver 规范的内置_可扩展性_。 结果是,无论您想要自动化哪个平台,当您使用 Appium 时,您将使用标准 WebDriver 规范,但有两个注意事项:
|
||||
当然,Appium 希望支持用户交互从 Web 到移动或 Web 到电视确实不同的情况,因此 Appium 还利用 WebDriver 规范的内置_可扩展性_。 结果是,无论您想要自动化哪个平台,当您使用 Appium 时,您将使用标准 WebDriver 规范,但有两个注意事项:
|
||||
|
||||
- 我们可能无法在给定平台上支持特定的 WebDriver API 命令,因此某些命令可能不受支持(例如,在原生移动应用自动化世界中,获取或设置 cookie 是不可能的)。
|
||||
- 我们可能支持超出 WebDriver API 命令列表的自动化行为,尽管任何此类命令将是有效的、符合规范的 WebDriver API 扩展。
|
||||
|
||||
您实际上如何_使用_ WebDriver API,特别是 Appium 的背景下? 我们将在下面的[部分](#universal-programming-language-access)中介绍 Appium 如何提供通用编程语言访问。 您现在只需要知道 Appium 通过实现 WebDriver 协议来引入通用 UI 自动化接口。
|
||||
您实际上如何_使用_ WebDriver API,特别是 Appium 的背景下? 我们将在下面的[部分](#universal-programming-language-access)中介绍 Appium 如何提供通用编程语言访问。 您现在只需要知道 Appium 通过实现 WebDriver 协议来引入通用 UI 自动化接口。
|
||||
|
||||
## 平台自动化行为
|
||||
|
||||
@@ -53,13 +53,13 @@ Appium 的最初目标是为移动应用(iOS 和 Android)开发自动化标
|
||||
|
||||
驱动程序是独立、可插拔模块的原因之一是它们彼此完全不同。 构建和使用不同平台的驱动程序的工具和要求完全不同。 因此,Appium 让您只使用您自动化任务所需的驱动程序。 选择驱动程序并安装它们以便与您的 Appium 实例一起使用非常重要,以至于 Appium 有自己的 [CLI 来管理驱动程序](../reference/cli/extensions.md)。
|
||||
|
||||
因此,为了回答我们最初的问题,Appium 为给定平台提供自动化功能访问的方式是 Appium 团队(或任何人[^3])为该平台编写一个_驱动程序_,实现 WebDriver 协议的尽可能多或少的部分。 然后,任何使用 Appium 的人都可以安装驱动程序。
|
||||
因此,为了回答我们最初的问题,Appium 为给定平台提供自动化功能访问的方式是 Appium 团队(或任何人[^3])为该平台编写一个_驱动程序_,实现 WebDriver 协议的尽可能多或少的部分。 然后,任何使用 Appium 的人都可以安装驱动程序。
|
||||
|
||||
[^3]: 您可以构建和分享自己的驱动程序! 查看[构建驱动程序](../developing/build-drivers.md)以了解更多关于如何在 Node.js 中开发可与 Appium 一起使用的驱动程序的信息。
|
||||
|
||||
## 通用编程语言访问
|
||||
|
||||
但使用 Appium 意味着什么,或者看起来像什么? 由于 Appium 最终是一个 Node.js 程序,它_可以_看起来像将 Appium 及其驱动程序作为库导入到您自己的 Node.js 程序中。 但这不会满足 Appium 的目标,即为使用任何流行编程语言的人提供自动化功能。
|
||||
但使用 Appium 意味着什么,或者看起来像什么? 由于 Appium 最终是一个 Node.js 程序,它_可以_看起来像将 Appium 及其驱动程序作为库导入到您自己的 Node.js 程序中。 但这不会满足 Appium 的目标,即为使用任何流行编程语言的人提供自动化功能。
|
||||
|
||||
幸运的是,Appium 继承 Selenium 的遗产意味着我们从一开始就有了这个问题的解决方案。 您看,WebDriver 规范实际上是一个基于 HTTP 的协议,这意味着它被设计为通过网络而不是单个程序的内存使用。
|
||||
|
||||
@@ -67,16 +67,16 @@ Appium 的最初目标是为移动应用(iOS 和 Android)开发自动化标
|
||||
|
||||
这里有几个重要的点供 Appium 用户了解:
|
||||
|
||||
- Appium 是一个_HTTP 服务器_。 只要您想使用它进行自动化,它必须在某台计算机上作为进程运行。 它必须在网络上可访问,以方便您想要运行自动化的任何计算机访问(无论是同一台机器还是世界另一端的机器)。
|
||||
- Appium 是一个_HTTP 服务器_。 只要您想使用它进行自动化,它必须在某台计算机上作为进程运行。 它必须在网络上可访问,以方便您想要运行自动化的任何计算机访问(无论是同一台机器还是世界另一端的机器)。
|
||||
- 除非您想编写原始 HTTP 调用或使用 cURL,否则请使用您选择的语言的 [Appium 客户端](clients.md)进行自动化。 每个客户端的目标是将 WebDriver 协议封装,以便您不必担心协议本身,而是可以使用感觉适合您语言的对象和方法。
|
||||
- Appium 服务器和 Appium 客户端_不需要_在同一台计算机上运行。 您只需能够通过某个网络从客户端向服务器发送 HTTP 请求。
|
||||
- Appium 服务器和 Appium 客户端_不需要_在同一台计算机上运行。 您只需能够通过某个网络从客户端向服务器发送 HTTP 请求。
|
||||
这大大促进了云提供商对 Appium 的使用,因为他们可以托管 Appium 服务器以及任何相关驱动程序和设备,您只需将客户端脚本指向他们的安全端点。
|
||||
|
||||
当然,这一切都不是关于“测试”本身,只是出于自动化目的使用 Appium 及其客户端库。 如果您想出于“测试”目的进行自动化,您可能希望利用测试运行器、测试框架等的帮助,这些都不需要与 Appium 相关;Appium“通用可访问性”的好处之一是它与您发现最适合您情况的任何工具集配合良好。
|
||||
|
||||
## Appium 的巨大范围
|
||||
|
||||
Appium 的愿景(在单一 API 下自动化一切)是巨大的! 当然,比开源项目的核心维护者团队大得多。 那么 Appium 如何希望实现这一目标? 基本上,通过授权社区在 Appium 之上作为_平台_开发功能。 这就是我们所谓的 Appium“生态系统”。
|
||||
Appium 的愿景(在单一 API 下自动化一切)是巨大的! 当然,比开源项目的核心维护者团队大得多。 那么 Appium 如何希望实现这一目标? 基本上,通过授权社区在 Appium 之上作为_平台_开发功能。 这就是我们所谓的 Appium“生态系统”。
|
||||
|
||||
Appium 团队确实自己维护了一些驱动程序(例如,我们之前讨论的 XCUITest 驱动程序)。 但它无法拥有特定于平台的专业知识或维护许多不同平台的驱动程序的能力。 但我们所做的,特别是从 Appium 2 开始,是提供工具来授权社区加入我们的愿景:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ hide:
|
||||
title: Appium 客户端简介
|
||||
---
|
||||
|
||||
出于[概述](./appium.md)中讨论的所有原因,Appium 基于 [W3C WebDriver 规范](https://w3c.github.io/webdriver/)。 这意味着 Appium 实现了客户端-服务器架构。 服务器(由 Appium 本身以及您用于自动化的任何驱动程序或插件组成)连接到测试设备,并实际负责在这些设备上实现自动化。 客户端(由_您_驱动,Appium 测试作者)负责通过网络向服务器发送命令,并接收来自服务器的响应。 这些响应可用于判断自动化命令是否成功,或可能包含您查询的关于应用程序状态的信息。 本文档是对客户端方面的概念性介绍。
|
||||
出于[概述](./appium.md)中讨论的所有原因,Appium 基于 [W3C WebDriver 规范](https://w3c.github.io/webdriver/)。 这意味着 Appium 实现了客户端-服务器架构。 服务器(由 Appium 本身以及您用于自动化的任何驱动程序或插件组成)连接到测试设备,并实际负责在这些设备上实现自动化。 客户端(由_您_驱动,Appium 测试作者)负责通过网络向服务器发送命令,并接收来自服务器的响应。 这些响应可用于判断自动化命令是否成功,或可能包含您查询的关于应用程序状态的信息。 本文档是对客户端方面的概念性介绍。
|
||||
|
||||
!!! info
|
||||
|
||||
@@ -20,7 +20,7 @@ title: Appium 客户端简介
|
||||
- 获取页面源代码
|
||||
- 截取屏幕截图
|
||||
|
||||
如果您查看 WebDriver 规范中的这些命令,您会注意到它们不是以任何特定编程语言定义的。 它们不是 Java 命令、JavaScript 命令或 Python 命令。 相反,它们形成了可以通过_任何_编程语言访问的 HTTP API( 如果您愿意,您可以只使用 cURL)。
|
||||
如果您查看 WebDriver 规范中的这些命令,您会注意到它们不是以任何特定编程语言定义的。 它们不是 Java 命令、JavaScript 命令或 Python 命令。 相反,它们形成了可以通过_任何_编程语言访问的 HTTP API( 如果您愿意,您可以只使用 cURL)。
|
||||
|
||||
例如,`查找元素`命令对应于发送到 HTTP 端点 `/session/:sessionid/element` 的 HTTP `POST` 请求(其中`:sessionid` 是服务器在之前 `创建会话` 调用中生成的唯一会话 ID 的占位符)。
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class MyNewDriver extends BaseDriver {
|
||||
}
|
||||
```
|
||||
|
||||
这个空驱动程序不会_做_任何事情,但你可以将它打包成一个 Node.js 模块,在模块的清单文件(`package.json`)中添加一些与 Appium 相关的字段,然后使用 `appium driver install` 安装它。
|
||||
这个空驱动程序不会_做_任何事情,但你可以将它打包成一个 Node.js 模块,在模块的清单文件(`package.json`)中添加一些与 Appium 相关的字段,然后使用 `appium driver install` 安装它。
|
||||
|
||||
因此,从技术角度来看,Appium 驱动程序只是一些继承自其他 Appium 代码的代码。 就是这样! 现在,继承自 `BaseDriver` 实际上给了我们很多东西,因为 `BaseDriver` 本质上是整个 WebDriver 协议的封装。 所以驱动程序要做一些有用的事情,只需要_实现_名称与其 WebDriver 协议等效的 Node.js 方法。
|
||||
因此,从技术角度来看,Appium 驱动程序只是一些继承自其他 Appium 代码的代码。 就是这样! 现在,继承自 `BaseDriver` 实际上给了我们很多东西,因为 `BaseDriver` 本质上是整个 WebDriver 协议的封装。 所以驱动程序要做一些有用的事情,只需要_实现_名称与其 WebDriver 协议等效的 Node.js 方法。
|
||||
|
||||
假设我想用这个空驱动程序做点什么;首先我必须决定要实现哪个 WebDriver 命令。 对于我们的例子,让我们选择[Navigate To](https://w3c.github.io/webdriver/#navigate-to) WebDriver 命令。 暂时不考虑当这个命令执行时我想让驱动程序_做_什么。 要告诉 Appium 驱动程序可以处理这个命令,我们所要做的就是在驱动程序类中定义一个像这样的方法:[^1]
|
||||
假设我想用这个空驱动程序做点什么;首先我必须决定要实现哪个 WebDriver 命令。 对于我们的例子,让我们选择[Navigate To](https://w3c.github.io/webdriver/#navigate-to) WebDriver 命令。 暂时不考虑当这个命令执行时我想让驱动程序_做_什么。 要告诉 Appium 驱动程序可以处理这个命令,我们所要做的就是在驱动程序类中定义一个像这样的方法:[^1]
|
||||
|
||||
```js
|
||||
async setUrl(url) {
|
||||
@@ -40,7 +40,7 @@ async setUrl(url) {
|
||||
- React 应用:加载特定路由
|
||||
- Unity:转到指定场景
|
||||
|
||||
因此,你可以看到,不同平台上驱动程序实现相同 WebDriver 命令的方式可能存在很大差异。[^2] 然而,_相同_的是它们表达可以处理协议命令的方式。
|
||||
因此,你可以看到,不同平台上驱动程序实现相同 WebDriver 命令的方式可能存在很大差异。[^2] 然而,_相同_的是它们表达可以处理协议命令的方式。
|
||||
|
||||
[^2]: 当然,我们希望保持语义尽可能相似,但在 iOS 的世界里,例如,通过深度链接(具有特殊应用特定方案的 URL)启动应用是我们最接近导航到 Web URL 的方式。
|
||||
|
||||
@@ -48,7 +48,7 @@ async setUrl(url) {
|
||||
|
||||
## 自动化映射
|
||||
|
||||
但_通常_驱动程序作者想要做的是为给定的平台提供自动化行为,这些行为在语义上与浏览器的 WebDriver 规范实现非常相似。 当你想查找一个元素时,你应该得到一个 UI 元素的引用。 当你想点击或轻触该元素时,产生的行为应该与人点击或轻触该元素相同。 等等。
|
||||
但_通常_驱动程序作者想要做的是为给定的平台提供自动化行为,这些行为在语义上与浏览器的 WebDriver 规范实现非常相似。 当你想查找一个元素时,你应该得到一个 UI 元素的引用。 当你想点击或轻触该元素时,产生的行为应该与人点击或轻触该元素相同。 等等。
|
||||
|
||||
因此,驱动程序作者面临的真正挑战不是如何使用 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 接口中,是驱动程序开发这项非常有用(但极具挑战性)的艺术!
|
||||
|
||||
@@ -90,7 +90,7 @@ WebDriver 协议! 没错,XCUITest 驱动程序的 Objective-C 端本身就
|
||||
这为 Appium 在某些情况下走捷径开辟了可能性。
|
||||
让我们想象一下 XCUITest 驱动程序需要实现 `Click Element` 命令。 这个实现的内部代码看起来就像是获取适当的参数并构造一个到 WebDriverAgent 服务器的 HTTP 请求。 在这种情况下,我们基本上只是在重建客户端对 Appium 服务器的原始调用![^4] 所以实际上没有必要编写一个函数来实现 `Click Element` 命令。 相反,XCUITest 驱动程序可以让 Appium 知道这个命令应该直接代理到其他 WebDriver 服务器。
|
||||
|
||||
[^4]: 这不是_完全_相同的调用,因为 Appium 服务器和 WebDriverAgent 服务器会生成不同的会话 ID,但这些差异会被透明地处理。
|
||||
[^4]: 这不是_完全_相同的调用,因为 Appium 服务器和 WebDriverAgent 服务器会生成不同的会话 ID,但这些差异会被透明地处理。
|
||||
|
||||
如果你不熟悉"代理"的概念,在这种情况下,它只是意味着 XCUITest 驱动程序根本不会参与处理该命令。 相反,它只会在协议级别被重新打包并转发到 WebDriverAgent,WebDriverAgent 的响应同样会直接传回客户端,而不会有任何 XCUITest 驱动程序代码看到或修改它。
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ hide:
|
||||
title: Appium 概览
|
||||
---
|
||||
|
||||
如主页所述,Appium 旨在支持_多个不同平台_(移动端、Web、桌面等)的 UI 自动化。 不仅如此,它还旨在支持用_不同语言_(JS、Java、Python 等)编写的自动化代码。 将所有这些功能整合到一个程序中是一项非常艰巨的任务,甚至可以说是不可能完成的任务!
|
||||
如主页所述,Appium 旨在支持_多个不同平台_(移动端、Web、桌面等)的 UI 自动化。 不仅如此,它还旨在支持用_不同语言_(JS、Java、Python 等)编写的自动化代码。 将所有这些功能整合到一个程序中是一项非常艰巨的任务,甚至可以说是不可能完成的任务!
|
||||
|
||||
为了实现这一目标,Appium 实际上被分为四个部分:
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ title: 安装 UiAutomator2 驱动程序
|
||||
像所有 Appium 驱动程序一样,UiAutomator 2 驱动程序通过 [Appium 扩展 CLI](../reference/cli/extensions.md) 安装。
|
||||
由于 UiAutomator 2 由核心 Appium 团队维护,它有一个“官方”驱动程序名称(`uiautomator2`),这使得安装更简单。
|
||||
|
||||
在安装之前,确保您的 Appium 服务器_未_运行(如果正在运行,请用 _Ctrl-C_ 退出)。
|
||||
在安装之前,确保您的 Appium 服务器_未_运行(如果正在运行,请用 _Ctrl-C_ 退出)。
|
||||
然后运行以下命令:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -107,6 +107,48 @@ Retrieves the [execute methods](../../guides/execute-methods.md) supported in th
|
||||
origin (driver-specific or plugin-specific). Refer to [the type definition file](https://github.com/appium/appium/blob/master/packages/types/lib/command.ts)
|
||||
for a detailed structure of this object.
|
||||
|
||||
### getCurrentAppiumContext
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
|
||||
### setAppiumContext
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/context
|
||||
```
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------ | -------------------------------------------- | ------ |
|
||||
| `name` | Name of the context to set as the active one | string |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumContexts
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/contexts
|
||||
```
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
### getLogEvents
|
||||
|
||||
```
|
||||
@@ -393,3 +435,71 @@ Retrieves data from a directory on the device under test.
|
||||
#### Response
|
||||
|
||||
`string` - the Base64-encoded zip file of the directory contents
|
||||
|
||||
### getAppiumRotation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
|
||||
| Name | Description | Type |
|
||||
| ---- | ---------------------------------------------------- | ------ |
|
||||
| `x` | Degrees by which the device is rotated on its X axis | number |
|
||||
| `y` | Degrees by which the device is rotated on its Y axis | number |
|
||||
| `z` | Degrees by which the device is rotated on its Z axis | number |
|
||||
|
||||
### setAppiumRotation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/rotation
|
||||
```
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ---- | ---------------------------------------------------- | ------ |
|
||||
| `x` | Degrees by which the device is rotated on its X axis | number |
|
||||
| `y` | Degrees by which the device is rotated on its Y axis | number |
|
||||
| `z` | Degrees by which the device is rotated on its Z axis | number |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
### getAppiumOrientation
|
||||
|
||||
```
|
||||
GET /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
|
||||
### setAppiumOrientation
|
||||
|
||||
```
|
||||
POST /session/:sessionId/appium/device/orientation
|
||||
```
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| `orientation` | New device orientation. Supported values are `PORTRAIT` or `LANDSCAPE`. | string |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
@@ -140,6 +140,10 @@ GET /session/:sessionId/orientation
|
||||
|
||||
Retrieves the current orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumOrientation`](./appium.md#getappiumorientation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - either `PORTRAIT` or `LANDSCAPE`
|
||||
@@ -154,6 +158,10 @@ POST /session/:sessionId/orientation
|
||||
|
||||
Sets the orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumOrientation`](./appium.md#setappiumorientation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
|
||||
@@ -21,6 +21,10 @@ GET /session/:sessionId/rotation
|
||||
|
||||
Retrieves the current spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumRotation`](./appium.md#getappiumrotation) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`Rotation` - an object with the following properties:
|
||||
@@ -41,6 +45,10 @@ POST /session/:sessionId/rotation
|
||||
|
||||
Sets the spatial orientation of the device under test.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumRotation`](./appium.md#setappiumrotation) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
@@ -63,6 +71,10 @@ GET /session/:sessionId/context
|
||||
|
||||
Retrieves the active application context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getCurrentAppiumContext`](./appium.md#getcurrentappiumcontext) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string` - the name of the active context
|
||||
@@ -77,6 +89,10 @@ POST /session/:sessionId/context
|
||||
|
||||
Sets an application context as the active context.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`setAppiumContext`](./appium.md#setappiumcontext) instead
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Description | Type |
|
||||
@@ -97,6 +113,10 @@ GET /session/:sessionId/contexts
|
||||
|
||||
Retrieves all available application contexts.
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use [`getAppiumContexts`](./appium.md#getappiumcontexts) instead
|
||||
|
||||
#### Response
|
||||
|
||||
`string[]` - the names of available contexts
|
||||
|
||||
@@ -170,10 +170,17 @@ Modifies the [`createSession`](./webdriver.md#createsession) endpoint:
|
||||
All endpoints for this plugin can be invoked without creating a session, allowing you to prepare
|
||||
your test environment in advance.
|
||||
|
||||
!!! info
|
||||
|
||||
Prior to plugin version 1.2.0, all endpoints below were mounted under the `/storage` prefix
|
||||
(e.g. `/storage/add`) instead of `/appium/storage`. The `/storage`-prefixed routes are still
|
||||
available for backward compatibility, but are deprecated and will be removed in a future
|
||||
version of the plugin.
|
||||
|
||||
### addStorageItem
|
||||
|
||||
```
|
||||
POST /storage/add
|
||||
POST /appium/storage/add
|
||||
```
|
||||
|
||||
Adds a new file to the storage.
|
||||
@@ -200,8 +207,8 @@ Example:
|
||||
```json
|
||||
{
|
||||
"ws": {
|
||||
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
"stream": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/appium/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
},
|
||||
"ttlMs": 300000
|
||||
}
|
||||
@@ -210,7 +217,7 @@ Example:
|
||||
### deleteStorageItem
|
||||
|
||||
```
|
||||
POST /storage/delete
|
||||
POST /appium/storage/delete
|
||||
```
|
||||
|
||||
Deletes a file in the storage.
|
||||
@@ -229,7 +236,7 @@ the storage, or if the delete request is invalid.
|
||||
### listStorageItems
|
||||
|
||||
```
|
||||
GET /storage/list
|
||||
GET /appium/storage/list
|
||||
```
|
||||
|
||||
List all files present in the storage.
|
||||
@@ -247,7 +254,7 @@ List all files present in the storage.
|
||||
### resetStorage
|
||||
|
||||
```
|
||||
POST /storage/reset
|
||||
POST /appium/storage/reset
|
||||
```
|
||||
|
||||
Deletes all uploaded files and stops any incomplete uploads.
|
||||
|
||||
@@ -159,14 +159,14 @@ extensions include scripts.
|
||||
#### Usage
|
||||
|
||||
```
|
||||
appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium {driver|plugin} run <extension-name> [<script-name> [<script-args>]]
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
| ---------------- | --------------------------------------------- |
|
||||
| `extension-name` | The short name of the installed extension |
|
||||
| `script-name` | The name of the script to be run |
|
||||
| `script-args` | Any additional arguments passed to the script |
|
||||
| Argument | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `extension-name` | The short name of the installed extension |
|
||||
| `script-name` | The name of the script to run. If not provided, a list of available scripts is returned. |
|
||||
| `script-args` | Any additional arguments passed to the script |
|
||||
|
||||
#### Options
|
||||
|
||||
@@ -182,6 +182,12 @@ appium {driver|plugin} run <extension-name> <script-name> [script-args]
|
||||
appium driver run uiautomator2 reset
|
||||
```
|
||||
|
||||
- List all available scripts included in the XCUITest driver:
|
||||
|
||||
```
|
||||
appium driver run xcuitest
|
||||
```
|
||||
|
||||
## `update`
|
||||
|
||||
Updates one or more extensions. Only supported for extensions installed via `npm`. By default,
|
||||
|
||||
@@ -29,6 +29,7 @@ appium
|
||||
| `--address`, `-a` | IPv4/IPv6 address to listen on | string | `0.0.0.0` |
|
||||
| `--allow-cors` | Allow web browser connections from any host | boolean | `false` |
|
||||
| `--allow-insecure` | List of [insecure features](../../guides/security.md) that should be allowed in this server's sessions. Individual features can be overridden by `--deny-insecure`. Has no effect in combination with `--relaxed-security`. | array<string> | `[]` |
|
||||
| `--allow-unknown-args` | Do not exit if unrecognized command-line arguments are passed to the server; ignore them instead. Useful when the Appium CLI is wrapped by external tooling that appends extra flags. | boolean | `false` |
|
||||
| `--base-path`, `-pa` | Base path to use as the prefix for all webdriver routes running on the server | string | `""` |
|
||||
| `--callback-address`, `-ca` | Callback IP address | string | `0.0.0.0` |
|
||||
| `--callback-port`, `-cp` | Callback port | integer | `4723` |
|
||||
@@ -64,7 +65,7 @@ appium
|
||||
| `--tmp` | Absolute path to the directory used for temporary files | string | [`os.tmpdir()`](https://nodejs.org/api/os.html#ostmpdir) |
|
||||
| `--use-drivers` | List of drivers to activate. By default, all installed drivers are activated. | array<string> | `[]` |
|
||||
| `--use-plugins` | List of plugins to activate. By default, no plugins are activated. Set to `["all"]` to activate all installed plugins. | array<string> | `[]` |
|
||||
| `--webhook`, `-G` | URL for an HTTP listener where the server logs should be output. This does not affect output on the console. | string | |
|
||||
| `--webhook`, `-G` | URL for an HTTP listener where the server logs should be output. This does not affect output on the console. A bare `host:port` is also accepted, which posts the logs to the root path over plain http; a value having neither a scheme nor a port falls back to `127.0.0.1:9003`. | string | |
|
||||
|
||||
### Info Options
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ title: Additional Resources
|
||||
|
||||
Here you can find links to additional Appium resources around the web:
|
||||
|
||||
## Websites
|
||||
|
||||
- [Appium Pro](https://appiumpro.com) - a blog and newsletter written by one of Appium's
|
||||
maintainers, Jonathan Lipps, with lots of useful guides
|
||||
|
||||
## Online Courses
|
||||
|
||||
- [Appium and Selenium Fundamentals](https://ui.headspin.io/university/learn/appium-selenium-fundamentals-2020) - a comprehensive video course on learning Python, Selenium, and Appium by Jonathan Lipps
|
||||
|
||||
+71
-101
@@ -1,52 +1,48 @@
|
||||
import type WebSocket from 'ws';
|
||||
import {
|
||||
AppiumIpc,
|
||||
BaseDriver,
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
DriverCore,
|
||||
errors,
|
||||
type ExtensionCore,
|
||||
generateDriverLogPrefix,
|
||||
GET_STATUS_COMMAND,
|
||||
isSessionCommand,
|
||||
isW3cCaps,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
promoteAppiumOptions,
|
||||
promoteAppiumOptionsForObject,
|
||||
PROTOCOLS,
|
||||
} from '@appium/base-driver';
|
||||
import {util} from '@appium/support';
|
||||
import type {
|
||||
AppiumServer,
|
||||
DriverCaps,
|
||||
DriverData,
|
||||
DriverOpts,
|
||||
ExternalDriver,
|
||||
IAppiumIpc,
|
||||
Plugin,
|
||||
PluginClass,
|
||||
PluginCommand,
|
||||
Protocol,
|
||||
RouteMatcher,
|
||||
IAppiumIpc,
|
||||
StringRecord,
|
||||
TimestampedMultiSessionData,
|
||||
W3CDriverCaps,
|
||||
} from '@appium/types';
|
||||
import {
|
||||
BaseDriver,
|
||||
DriverCore,
|
||||
type ExtensionCore,
|
||||
errors,
|
||||
isSessionCommand,
|
||||
PROTOCOLS,
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
promoteAppiumOptions,
|
||||
promoteAppiumOptionsForObject,
|
||||
generateDriverLogPrefix,
|
||||
isW3cCaps,
|
||||
} from '@appium/base-driver';
|
||||
import {APPIUM_VER, getBuildInfo, updateBuildInfo} from './helpers/build';
|
||||
import {
|
||||
makeNonW3cCapsError,
|
||||
parseCapsForInnerDriver,
|
||||
pullSettings,
|
||||
type ParsedDriverCaps,
|
||||
} from './helpers/capability';
|
||||
import {util} from '@appium/support';
|
||||
import {getDefaultsForExtension} from './schema';
|
||||
import {DRIVER_TYPE, BIDI_BASE_PATH, SESSION_DISCOVERY_FEATURE} from './constants';
|
||||
import type WebSocket from 'ws';
|
||||
|
||||
import * as bidiCommands from './bidi-commands';
|
||||
import {BIDI_BASE_PATH, DRIVER_TYPE, SESSION_DISCOVERY_FEATURE} from './constants';
|
||||
import type {DriverConfig} from './extension/driver-config';
|
||||
import {APPIUM_VER, getBuildInfo, updateBuildInfo} from './helpers/build';
|
||||
import {makeNonW3cCapsError, parseCapsForInnerDriver, type ParsedDriverCaps, pullSettings} from './helpers/capability';
|
||||
import * as insecureFeatures from './insecure-features';
|
||||
import * as inspectorCommands from './inspector-commands';
|
||||
import type {DriverConfig} from './extension/driver-config';
|
||||
import {AppiumIpc} from '@appium/base-driver';
|
||||
import {getDefaultsForExtension} from './schema';
|
||||
import {compact, pickBy, pull} from './utils';
|
||||
|
||||
const desiredCapabilityConstraints = {
|
||||
@@ -76,6 +72,19 @@ type SessionHandlerCreateResult = SessionHandlerResult<
|
||||
|
||||
type SessionHandlerDeleteResult = SessionHandlerResult<void>;
|
||||
|
||||
/**
|
||||
* Tuple shape of the deprecated multi-argument overload of {@link AppiumDriver.createSession}.
|
||||
* Shared between that overload's declaration and its implementation signature so the parameter
|
||||
* list only needs to be written out once.
|
||||
*
|
||||
* @deprecated Use the single-argument overload of {@link AppiumDriver.createSession} instead.
|
||||
*/
|
||||
type LegacyCreateSessionArgs = [
|
||||
w3cCapabilities1: W3CAppiumDriverCaps,
|
||||
w3cCapabilities2?: W3CAppiumDriverCaps,
|
||||
w3cCapabilities3?: W3CAppiumDriverCaps,
|
||||
];
|
||||
|
||||
/** @internal Not part of {@link ExternalDriver}; used only when wiring session IPC. */
|
||||
type IpcAssignable = {
|
||||
assignIpc?: (ipc: IAppiumIpc) => Promise<void>;
|
||||
@@ -218,11 +227,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
}
|
||||
|
||||
/** Logs BaseDriver version lines when starting a new inner-driver session. */
|
||||
printNewSessionAnnouncement(
|
||||
driverName: string,
|
||||
driverVersion?: string,
|
||||
driverBaseVersion?: string,
|
||||
): void {
|
||||
printNewSessionAnnouncement(driverName: string, driverVersion?: string, driverBaseVersion?: string): void {
|
||||
this.log.info(
|
||||
driverVersion
|
||||
? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
|
||||
@@ -271,19 +276,22 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
|
||||
/**
|
||||
* Creates a session: picks an inner driver from caps, runs plugin hooks, and returns a protocol
|
||||
* envelope with either `[sessionId, caps, protocol]` or an error. Legacy call sites may pass the
|
||||
* same W3C caps in up to three positions; the first W3C-shaped value wins.
|
||||
* envelope with either `[sessionId, caps, protocol]` or an error.
|
||||
*
|
||||
* @param w3cCapabilities - the new session capabilities in W3C format
|
||||
*/
|
||||
async createSession(
|
||||
w3cCapabilities1: W3CAppiumDriverCaps,
|
||||
w3cCapabilities2?: W3CAppiumDriverCaps,
|
||||
w3cCapabilities3?: W3CAppiumDriverCaps,
|
||||
): Promise<SessionHandlerCreateResult> {
|
||||
async createSession(w3cCapabilities: W3CAppiumDriverCaps): Promise<SessionHandlerCreateResult>;
|
||||
/**
|
||||
* @deprecated Legacy call sites may pass the same W3C caps in up to three positions. These
|
||||
* positions are intended to carry the same value; if they differ, which one wins is
|
||||
* unspecified. Use the single-argument overload of {@linkcode createSession} instead.
|
||||
*/
|
||||
async createSession(...legacyArgs: LegacyCreateSessionArgs): Promise<SessionHandlerCreateResult>;
|
||||
async createSession(...legacyArgs: LegacyCreateSessionArgs): Promise<SessionHandlerCreateResult> {
|
||||
const [w3cCapabilities1, w3cCapabilities2, w3cCapabilities3] = legacyArgs;
|
||||
const defaultCapabilities = structuredClone(this.args.defaultCapabilities);
|
||||
const defaultSettings = pullSettings((defaultCapabilities ?? {}) as StringRecord);
|
||||
const w3cCapabilities = structuredClone(
|
||||
[w3cCapabilities3, w3cCapabilities2, w3cCapabilities1].find(isW3cCaps),
|
||||
);
|
||||
const w3cCapabilities = structuredClone([w3cCapabilities3, w3cCapabilities2, w3cCapabilities1].find(isW3cCaps));
|
||||
if (!w3cCapabilities) {
|
||||
throw makeNonW3cCapsError();
|
||||
}
|
||||
@@ -309,8 +317,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
if ('error' in parsedCaps && parsedCaps.error) {
|
||||
throw parsedCaps.error;
|
||||
}
|
||||
const {desiredCaps, processedW3CCapabilities} =
|
||||
parsedCaps as ParsedDriverCaps<AppiumDriverConstraints>;
|
||||
const {desiredCaps, processedW3CCapabilities} = parsedCaps as ParsedDriverCaps<AppiumDriverConstraints>;
|
||||
|
||||
const {
|
||||
driver: InnerDriver,
|
||||
@@ -360,9 +367,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
throw new errors.SessionNotCreatedError(msg);
|
||||
}
|
||||
this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
|
||||
otherPendingDriversData = compact(
|
||||
this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData),
|
||||
);
|
||||
otherPendingDriversData = compact(this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData));
|
||||
this.pendingDrivers[InnerDriver.name].push(driverInstance);
|
||||
|
||||
try {
|
||||
@@ -401,8 +406,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
// apply initial values to Appium settings (if provided)
|
||||
if (driverInstance.isW3CProtocol() && !util.isEmpty(w3cSettings)) {
|
||||
this.log.info(
|
||||
`Applying the initial values to Appium settings parsed from W3C caps: ` +
|
||||
JSON.stringify(w3cSettings),
|
||||
`Applying the initial values to Appium settings parsed from W3C caps: ` + JSON.stringify(w3cSettings),
|
||||
);
|
||||
await driverInstance.updateSettings(w3cSettings);
|
||||
}
|
||||
@@ -448,15 +452,11 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
if (this.sessionPlugins[innerSessionId]) {
|
||||
for (const plugin of this.sessionPlugins[innerSessionId]) {
|
||||
if (typeof plugin.onUnexpectedShutdown === 'function') {
|
||||
this.log.debug(
|
||||
`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`,
|
||||
);
|
||||
this.log.debug(`Plugin ${plugin.name} defines an unexpected shutdown handler; calling it now`);
|
||||
try {
|
||||
await plugin.onUnexpectedShutdown(driver, cause);
|
||||
} catch (e) {
|
||||
this.log.warn(
|
||||
`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`,
|
||||
);
|
||||
this.log.warn(`Got an error when running plugin ${plugin.name} shutdown handler: ${e}`);
|
||||
}
|
||||
} else {
|
||||
this.log.debug(`Plugin ${plugin.name} does not define an unexpected shutdown handler`);
|
||||
@@ -492,8 +492,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
for (const datum of data) {
|
||||
if (!datum) {
|
||||
throw new Error(
|
||||
`Problem getting session data for driver type ` +
|
||||
`${InnerDriver.name}; does it implement 'get driverData'?`,
|
||||
`Problem getting session data for driver type ` + `${InnerDriver.name}; does it implement 'get driverData'?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -512,9 +511,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
if (this.sessions[sessionId]) {
|
||||
const curConstructorName = this.sessions[sessionId].constructor.name;
|
||||
otherSessionsData = Object.entries(this.sessions)
|
||||
.filter(
|
||||
([key, value]) => value.constructor.name === curConstructorName && key !== sessionId,
|
||||
)
|
||||
.filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
|
||||
.map(([, value]) => value.driverData);
|
||||
dstSession = this.sessions[sessionId];
|
||||
protocol = dstSession.protocol;
|
||||
@@ -560,9 +557,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
const {force = false, reason} = opts;
|
||||
this.log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
|
||||
const cleanupPromises = force
|
||||
? Object.values(this.sessions).map((drv) =>
|
||||
drv.startUnexpectedShutdown(reason ? new Error(reason) : undefined),
|
||||
)
|
||||
? Object.values(this.sessions).map((drv) => drv.startUnexpectedShutdown(reason ? new Error(reason) : undefined))
|
||||
: Object.keys(this.sessions).map((id) => this.deleteSession(id));
|
||||
for (const cleanupPromise of cleanupPromises) {
|
||||
try {
|
||||
@@ -587,9 +582,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
if (!driver) {
|
||||
return [];
|
||||
}
|
||||
return (this.sessionPlugins[sessionId] = this.createPluginInstances(
|
||||
generateDriverLogPrefix(driver),
|
||||
));
|
||||
return (this.sessionPlugins[sessionId] = this.createPluginInstances(generateDriverLogPrefix(driver)));
|
||||
}
|
||||
|
||||
if (util.isEmpty(this.sessionlessPlugins)) {
|
||||
@@ -682,9 +675,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
// driver's executeCommand method will be called, which means we can't guarantee that the
|
||||
// newCommandTimeout will be cleared. So we do it here as well.
|
||||
if (plugins.length && dstSession) {
|
||||
this.log.debug(
|
||||
'Clearing new command timeout pre-emptively since plugin(s) will handle this command',
|
||||
);
|
||||
this.log.debug('Clearing new command timeout pre-emptively since plugin(s) will handle this command');
|
||||
await dstSession.clearNewCommandTimeout();
|
||||
}
|
||||
|
||||
@@ -719,11 +710,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
if (!dstSession?.proxyCommand) {
|
||||
throw new NoDriverProxyCommandError();
|
||||
}
|
||||
return await dstSession.proxyCommand(
|
||||
reqForProxy.originalUrl,
|
||||
reqForProxy.method,
|
||||
reqForProxy.body,
|
||||
);
|
||||
return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method, reqForProxy.body);
|
||||
}
|
||||
|
||||
if (isGetStatus) {
|
||||
@@ -760,15 +747,9 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
// if we had plugins, and if they did not ultimately call the default handler, this means our
|
||||
// new command timeout was not restarted by the default handler's executeCommand call, so
|
||||
// restart it here using the same logic as in BaseDriver's executeCommand
|
||||
if (
|
||||
dstSession &&
|
||||
!cmdHandledBy.default &&
|
||||
dstSession.isCommandsQueueEnabled &&
|
||||
cmd !== DELETE_SESSION_COMMAND
|
||||
) {
|
||||
if (dstSession && !cmdHandledBy.default && dstSession.isCommandsQueueEnabled && cmd !== DELETE_SESSION_COMMAND) {
|
||||
this.log.debug(
|
||||
'Restarting new command timeout via umbrella driver since plugin did not ' +
|
||||
'allow default handler to execute',
|
||||
'Restarting new command timeout via umbrella driver since plugin did not ' + 'allow default handler to execute',
|
||||
);
|
||||
await dstSession.startNewCommandTimeout();
|
||||
}
|
||||
@@ -786,9 +767,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
const promoted = this.sessionPlugins[newSessionId] as (Plugin & ExtensionCore)[];
|
||||
for (const p of promoted) {
|
||||
if (typeof p.updateLogPrefix === 'function') {
|
||||
p.updateLogPrefix(
|
||||
`${generateDriverLogPrefix(p)} <${generateDriverLogPrefix(this.sessions[newSessionId])}>`,
|
||||
);
|
||||
p.updateLogPrefix(`${generateDriverLogPrefix(p)} <${generateDriverLogPrefix(this.sessions[newSessionId])}>`);
|
||||
}
|
||||
// we also want to assign the IPC channel for this session to the plugins
|
||||
const extPlugin = p as unknown as IpcAssignable;
|
||||
@@ -837,12 +816,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
const cmdHandler = (plugin as Plugin & Record<string, unknown>)[cmd];
|
||||
if (typeof cmdHandler === 'function') {
|
||||
// Command methods must run with plugin as `this` (detached property access drops binding).
|
||||
return await (cmdHandler as PluginCommand).call(
|
||||
plugin,
|
||||
_next,
|
||||
driver as ExternalDriver,
|
||||
...args,
|
||||
);
|
||||
return await (cmdHandler as PluginCommand).call(plugin, _next, driver as ExternalDriver, ...args);
|
||||
}
|
||||
if (typeof plugin.handle !== 'function') {
|
||||
throw new Error(`Plugin ${plugin.name} cannot handle command '${cmd}'`);
|
||||
@@ -922,9 +896,7 @@ export class AppiumDriver extends DriverCore<AppiumDriverConstraints> {
|
||||
/** URL patterns the session driver does not want proxied; empty if no session or no list. */
|
||||
override getProxyAvoidList(sessionId: string): RouteMatcher[] {
|
||||
const dstSession = this.sessions[sessionId];
|
||||
return typeof dstSession?.getProxyAvoidList === 'function'
|
||||
? dstSession.getProxyAvoidList()
|
||||
: [];
|
||||
return typeof dstSession?.getProxyAvoidList === 'function' ? dstSession.getProxyAvoidList() : [];
|
||||
}
|
||||
|
||||
/** Whether the session driver supports proxying for this session. */
|
||||
@@ -961,8 +933,6 @@ export class NoDriverProxyCommandError extends Error {
|
||||
function isAppiumDriverCommand(cmd: string): boolean {
|
||||
return (
|
||||
!isSessionCommand(cmd) ||
|
||||
[DELETE_SESSION_COMMAND, LIST_DRIVER_COMMANDS_COMMAND, LIST_DRIVER_EXTENSIONS_COMMAND].includes(
|
||||
cmd,
|
||||
)
|
||||
[DELETE_SESSION_COMMAND, LIST_DRIVER_COMMANDS_COMMAND, LIST_DRIVER_EXTENSIONS_COMMAND].includes(cmd)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import {util} from '@appium/support';
|
||||
import type {IncomingMessage} from 'node:http';
|
||||
import os from 'node:os';
|
||||
import {promisify} from 'node:util';
|
||||
import {capitalize} from './utils';
|
||||
|
||||
import type {ExtensionCore} from '@appium/base-driver';
|
||||
import {errors} from '@appium/base-driver';
|
||||
import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
|
||||
import WebSocket from 'ws';
|
||||
import os from 'node:os';
|
||||
import {isBroadcastIp, fetchInterfaces, V4_BROADCAST_IP} from './helpers/network';
|
||||
import type {IncomingMessage} from 'node:http';
|
||||
import type {AppiumDriver} from './appium';
|
||||
import {util} from '@appium/support';
|
||||
import type {
|
||||
ErrorBiDiCommandResponse,
|
||||
SuccessBiDiCommandResponse,
|
||||
ExternalDriver,
|
||||
StringRecord,
|
||||
Plugin,
|
||||
BiDiResultData,
|
||||
ErrorBiDiCommandResponse,
|
||||
ExternalDriver,
|
||||
Plugin,
|
||||
StringRecord,
|
||||
SuccessBiDiCommandResponse,
|
||||
} from '@appium/types';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import type {AppiumDriver} from './appium';
|
||||
import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
|
||||
import {fetchInterfaces, isBroadcastIp, V4_BROADCAST_IP} from './helpers/network';
|
||||
import {capitalize} from './utils';
|
||||
|
||||
type ExtensionPlugin = Plugin & ExtensionCore;
|
||||
type AnyDriver = ExternalDriver | AppiumDriver;
|
||||
@@ -50,9 +52,7 @@ export function determineBiDiHost(address: string): string {
|
||||
return address;
|
||||
}
|
||||
|
||||
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6).filter(
|
||||
(iface) => !iface.internal,
|
||||
);
|
||||
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6).filter((iface) => !iface.internal);
|
||||
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
|
||||
}
|
||||
|
||||
@@ -64,20 +64,14 @@ export function determineBiDiHost(address: string): string {
|
||||
export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
|
||||
try {
|
||||
const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
|
||||
const {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr} =
|
||||
initBiDiSocketFunc(ws, req);
|
||||
const {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
|
||||
ws,
|
||||
req,
|
||||
);
|
||||
|
||||
const initBidiSocketHandlersFunc: OmitThisParameter<typeof initBidiSocketHandlers> =
|
||||
initBidiSocketHandlers.bind(this);
|
||||
initBidiSocketHandlersFunc(
|
||||
ws,
|
||||
proxyClient,
|
||||
send,
|
||||
sendToProxy,
|
||||
bidiHandlerDriver,
|
||||
bidiHandlerPlugins,
|
||||
logSocketErr,
|
||||
);
|
||||
initBidiSocketHandlersFunc(ws, proxyClient, send, sendToProxy, bidiHandlerDriver, bidiHandlerPlugins, logSocketErr);
|
||||
if (proxyClient) {
|
||||
const initBidiProxyHandlersFunc: OmitThisParameter<typeof initBidiProxyHandlers> =
|
||||
initBidiProxyHandlers.bind(bidiHandlerDriver);
|
||||
@@ -116,27 +110,16 @@ export async function onBidiMessage(
|
||||
({id, method, params} = JSON.parse(data.toString('utf8')));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Could not parse Bidi command '${dataTruncated}': ${message}`,
|
||||
);
|
||||
throw new errors.InvalidArgumentError(`Could not parse Bidi command '${dataTruncated}': ${message}`);
|
||||
}
|
||||
driverLog.info(`--> BIDI message #${id}`);
|
||||
if (!method) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing method for BiDi operation in '${dataTruncated}'`,
|
||||
);
|
||||
throw new errors.InvalidArgumentError(`Missing method for BiDi operation in '${dataTruncated}'`);
|
||||
}
|
||||
if (!params) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing params for BiDi operation in '${dataTruncated}`,
|
||||
);
|
||||
throw new errors.InvalidArgumentError(`Missing params for BiDi operation in '${dataTruncated}`);
|
||||
}
|
||||
const executeWrappedCommand = wrapCommandWithPlugins(
|
||||
driver as ExtensionCore,
|
||||
plugins,
|
||||
method,
|
||||
params,
|
||||
);
|
||||
const executeWrappedCommand = wrapCommandWithPlugins(driver as ExtensionCore, plugins, method, params);
|
||||
const result = await executeWrappedCommand();
|
||||
resMessage = {
|
||||
id,
|
||||
@@ -150,9 +133,7 @@ export async function onBidiMessage(
|
||||
'bidiErrObject' in err &&
|
||||
typeof (err as {bidiErrObject: unknown}).bidiErrObject === 'function'
|
||||
) {
|
||||
resMessage = (
|
||||
err as {bidiErrObject: (msgId: number) => ErrorBiDiCommandResponse}
|
||||
).bidiErrObject(id);
|
||||
resMessage = (err as {bidiErrObject: (msgId: number) => ErrorBiDiCommandResponse}).bidiErrObject(id);
|
||||
} else {
|
||||
resMessage = {
|
||||
id,
|
||||
@@ -229,11 +210,7 @@ function wrapCommandWithPlugins(
|
||||
* @param ws The websocket connection object
|
||||
* @param req The connection pathname, which might include the session id
|
||||
*/
|
||||
function initBidiSocket(
|
||||
this: AppiumDriver,
|
||||
ws: WebSocket,
|
||||
req: IncomingMessage,
|
||||
): InitBiDiSocketResult {
|
||||
function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): InitBiDiSocketResult {
|
||||
const pathname = req.url;
|
||||
if (!pathname) {
|
||||
throw new Error('Invalid connection request: pathname missing from request');
|
||||
@@ -315,9 +292,7 @@ function initBidiSocket(
|
||||
// 1. Make it async-await friendly
|
||||
// 2. Do some logging if there's a send error
|
||||
const sendFactory = (socket: WebSocket) => {
|
||||
const socketSend = promisify(socket.send.bind(socket)) as (
|
||||
data: string | Buffer,
|
||||
) => Promise<void>;
|
||||
const socketSend = promisify(socket.send.bind(socket)) as (data: string | Buffer) => Promise<void>;
|
||||
return async (data: string | Buffer) => {
|
||||
try {
|
||||
await assertIsOpen(socket);
|
||||
@@ -347,12 +322,7 @@ function initBidiSocket(
|
||||
* @param send - a method used to send data to the
|
||||
* client
|
||||
*/
|
||||
function initBidiProxyHandlers(
|
||||
this: AnyDriver,
|
||||
proxyClient: WebSocket,
|
||||
ws: WebSocket,
|
||||
send: SendData,
|
||||
): void {
|
||||
function initBidiProxyHandlers(this: AnyDriver, proxyClient: WebSocket, ws: WebSocket, send: SendData): void {
|
||||
// Set up handlers for events that might come from the upstream bidi socket connection if
|
||||
// we're in proxy mode
|
||||
const driverLog = this.log;
|
||||
@@ -557,12 +527,7 @@ async function assertIsOpen(ws: WebSocket, timeoutMs: number = 5000): Promise<We
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
`The BiDi web socket at ${ws.url} did not ` + `open after ${timeoutMs}ms timeout`,
|
||||
),
|
||||
),
|
||||
() => reject(new Error(`The BiDi web socket at ${ws.url} did not ` + `open after ${timeoutMs}ms timeout`)),
|
||||
timeoutMs,
|
||||
);
|
||||
ws.once('error', reject);
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import {init as logsinkInit} from '../logsink';
|
||||
import {log as logger} from '../logger';
|
||||
import {util, env} from '@appium/support';
|
||||
import {env, util} from '@appium/support';
|
||||
import type {DriverOpts} from '@appium/types';
|
||||
import {defaultsDeep} from '../utils';
|
||||
import {AppiumDriver, type AppiumDriverConstraints} from '../appium';
|
||||
import {runExtensionCommand} from '../cli/extension';
|
||||
import {runSetupCommand} from '../cli/setup-command';
|
||||
import {getParser, ArgParser} from '../cli/parser';
|
||||
import {readConfigFile} from './config-file';
|
||||
import {getNonDefaultServerArgs, showConfig} from './startup-config';
|
||||
import {adjustNodePath, requireDir, showDebugInfo} from './node-helpers';
|
||||
import {loadExtensions, type ExtensionConfigs} from '../extension';
|
||||
import {SERVER_SUBCOMMAND} from '../constants';
|
||||
import {injectAppiumSymlinks} from '../cli/extension-command';
|
||||
import {getDefaultsForSchema} from '../schema/schema';
|
||||
import {
|
||||
isDriverCommandArgs,
|
||||
isExtensionCommandArgs,
|
||||
isPluginCommandArgs,
|
||||
isServerCommandArgs,
|
||||
isSetupCommandArgs,
|
||||
} from '../schema/cli-args-guards';
|
||||
import type {
|
||||
Args,
|
||||
CliCommand,
|
||||
@@ -29,8 +8,30 @@ import type {
|
||||
CliExtensionSubcommand,
|
||||
ParsedArgs,
|
||||
} from 'appium/types';
|
||||
import {determineAppiumHomeSource, preflightChecks} from './main-helpers';
|
||||
|
||||
import {AppiumDriver, type AppiumDriverConstraints} from '../appium';
|
||||
import {runExtensionCommand} from '../cli/extension';
|
||||
import {injectAppiumSymlinks} from '../cli/extension-command';
|
||||
import {ArgParser, getParser} from '../cli/parser';
|
||||
import {runSetupCommand} from '../cli/setup-command';
|
||||
import {SERVER_SUBCOMMAND} from '../constants';
|
||||
import {type ExtensionConfigs, loadExtensions} from '../extension';
|
||||
import {log as logger} from '../logger';
|
||||
import {init as logsinkInit} from '../logsink';
|
||||
import {
|
||||
isDriverCommandArgs,
|
||||
isExtensionCommandArgs,
|
||||
isPluginCommandArgs,
|
||||
isServerCommandArgs,
|
||||
isSetupCommandArgs,
|
||||
} from '../schema/cli-args-guards';
|
||||
import {getDefaultsForSchema} from '../schema/schema';
|
||||
import {defaultsDeep} from '../utils';
|
||||
import {readConfigFile} from './config-file';
|
||||
import type {InitResult, PreConfigArgs} from './init-types';
|
||||
import {determineAppiumHomeSource, preflightChecks} from './main-helpers';
|
||||
import {adjustNodePath, requireDir, showDebugInfo} from './node-helpers';
|
||||
import {getNonDefaultServerArgs, showConfig} from './startup-config';
|
||||
|
||||
/**
|
||||
* Parses CLI/programmatic args, loads config and extensions, and returns server-ready state or runs extension/setup flows.
|
||||
@@ -88,9 +89,7 @@ export class AppiumInitializer {
|
||||
private async parsePreConfigArgs<
|
||||
Cmd extends CliCommand,
|
||||
SubCmd extends CliExtensionSubcommand | CliCommandSetupSubcommand | void,
|
||||
>(
|
||||
args?: Args<Cmd, SubCmd>,
|
||||
): Promise<{preConfigArgs: PreConfigArgs; throwInsteadOfExit: boolean}> {
|
||||
>(args?: Args<Cmd, SubCmd>): Promise<{preConfigArgs: PreConfigArgs; throwInsteadOfExit: boolean}> {
|
||||
const parser = await getParser();
|
||||
let throwInsteadOfExit = false;
|
||||
|
||||
@@ -135,12 +134,7 @@ export class AppiumInitializer {
|
||||
) as unknown as ParsedArgs<CliCommandServer>;
|
||||
|
||||
if (preConfigArgs.showConfig) {
|
||||
showConfig(
|
||||
getNonDefaultServerArgs(preConfigArgs as Args),
|
||||
configResult,
|
||||
defaults,
|
||||
serverArgs,
|
||||
);
|
||||
showConfig(getNonDefaultServerArgs(preConfigArgs as Args), configResult, defaults, serverArgs);
|
||||
return {} as InitResult<Cmd>;
|
||||
}
|
||||
|
||||
@@ -177,14 +171,10 @@ export class AppiumInitializer {
|
||||
if (!serverArgs.logFilters) {
|
||||
return;
|
||||
}
|
||||
const {issues, rules} = await logger
|
||||
.unwrap()
|
||||
.loadSecureValuesPreprocessingRules(serverArgs.logFilters);
|
||||
const {issues, rules} = await logger.unwrap().loadSecureValuesPreprocessingRules(serverArgs.logFilters);
|
||||
const argToLog = util.truncateString(JSON.stringify(serverArgs.logFilters), {length: 150});
|
||||
if (!util.isEmpty(issues)) {
|
||||
throw new Error(
|
||||
`The log filtering rules config ${argToLog} has issues: ` + JSON.stringify(issues, null, 2),
|
||||
);
|
||||
throw new Error(`The log filtering rules config ${argToLog} has issues: ` + JSON.stringify(issues, null, 2));
|
||||
}
|
||||
if (util.isEmpty(rules)) {
|
||||
logger.warn(`Found no log filtering rules in the ${argToLog} config. ` + `Is that expected?`);
|
||||
@@ -209,9 +199,7 @@ export class AppiumInitializer {
|
||||
}
|
||||
|
||||
if (isDriverCommandArgs(preConfigArgs) || isPluginCommandArgs(preConfigArgs)) {
|
||||
const cmd = isDriverCommandArgs(preConfigArgs)
|
||||
? preConfigArgs.driverCommand
|
||||
: preConfigArgs.pluginCommand;
|
||||
const cmd = isDriverCommandArgs(preConfigArgs) ? preConfigArgs.driverCommand : preConfigArgs.pluginCommand;
|
||||
if (cmd === 'install') {
|
||||
await injectAppiumSymlinks(driverConfig, pluginConfig, logger);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import {log as logger} from '../logger';
|
||||
import net from 'node:net';
|
||||
|
||||
import type {ServerOpts} from '@appium/base-driver';
|
||||
import {util} from '@appium/support';
|
||||
import type {AppiumServer} from '@appium/types';
|
||||
import {getActivePlugins, getActiveDrivers} from '../extension';
|
||||
import type {Args, CliCommand, CliCommandServer, CliCommandSetupSubcommand, CliExtensionSubcommand} from 'appium/types';
|
||||
|
||||
import {getActiveDrivers, getActivePlugins} from '../extension';
|
||||
import {log as logger} from '../logger';
|
||||
import registerNode from './grid-v3-register';
|
||||
import type {InitResult, ServerInitData} from './init-types';
|
||||
import {
|
||||
determineAppiumHomeSource,
|
||||
logStartupInfo,
|
||||
buildServerOpts,
|
||||
createAppiumServer,
|
||||
determineAppiumHomeSource,
|
||||
logServerAddress,
|
||||
logStartupInfo,
|
||||
} from './main-helpers';
|
||||
import type {InitResult, ServerInitData} from './init-types';
|
||||
import type {
|
||||
Args,
|
||||
CliCommand,
|
||||
CliCommandServer,
|
||||
CliCommandSetupSubcommand,
|
||||
CliExtensionSubcommand,
|
||||
} from 'appium/types';
|
||||
import type {ServerOpts} from '@appium/base-driver';
|
||||
import net from 'node:net';
|
||||
|
||||
const MAX_SERVER_PROCESS_LISTENERS = 100;
|
||||
|
||||
@@ -45,8 +41,7 @@ export class AppiumMainRunner {
|
||||
return undefined as Cmd extends CliCommandServer ? AppiumServer : void;
|
||||
}
|
||||
|
||||
const {appiumDriver, pluginConfig, driverConfig, parsedArgs, appiumHome} =
|
||||
initResult as ServerInitData;
|
||||
const {appiumDriver, pluginConfig, driverConfig, parsedArgs, appiumHome} = initResult as ServerInitData;
|
||||
|
||||
const pluginClasses = await getActivePlugins(
|
||||
pluginConfig,
|
||||
@@ -69,12 +64,7 @@ export class AppiumMainRunner {
|
||||
parsedArgs.driversImportChunkSize,
|
||||
parsedArgs.useDrivers,
|
||||
);
|
||||
const {serverOpts, normalizedBasePath} = buildServerOpts(
|
||||
appiumDriver,
|
||||
parsedArgs,
|
||||
driverClasses,
|
||||
pluginClasses,
|
||||
);
|
||||
const {serverOpts, normalizedBasePath} = buildServerOpts(appiumDriver, parsedArgs, driverClasses, pluginClasses);
|
||||
|
||||
const server = await this.startHttpServer(serverOpts, appiumDriver, normalizedBasePath);
|
||||
if (!server) {
|
||||
@@ -131,12 +121,7 @@ export class AppiumMainRunner {
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (parsedArgs.nodeconfig) {
|
||||
await registerNode(
|
||||
parsedArgs.nodeconfig,
|
||||
parsedArgs.address,
|
||||
parsedArgs.port,
|
||||
normalizedBasePath,
|
||||
);
|
||||
await registerNode(parsedArgs.nodeconfig, parsedArgs.address, parsedArgs.port, normalizedBasePath);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
await server.close();
|
||||
@@ -144,10 +129,7 @@ export class AppiumMainRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private attachSignalHandlers(
|
||||
appiumDriver: ServerInitData['appiumDriver'],
|
||||
server: AppiumServer,
|
||||
): void {
|
||||
private attachSignalHandlers(appiumDriver: ServerInitData['appiumDriver'], server: AppiumServer): void {
|
||||
process.setMaxListeners(MAX_SERVER_PROCESS_LISTENERS);
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||
process.once(signal, async function onSignal() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {util} from '@appium/support';
|
||||
import type {AppiumConfig, NormalizedAppiumConfig} from '@appium/types';
|
||||
import type {IOutputError} from '@sidvind/better-ajv-errors';
|
||||
import type {ErrorObject, SchemaObject} from 'ajv';
|
||||
import {lilconfig, type LoaderSync, type LilconfigResult} from 'lilconfig';
|
||||
import {util} from '@appium/support';
|
||||
import {lilconfig, type LilconfigResult, type LoaderSync} from 'lilconfig';
|
||||
import * as yaml from 'yaml';
|
||||
import {camelCase, getPath, mapKeys, mapValues} from '../utils';
|
||||
import type {AppiumConfig, NormalizedAppiumConfig} from '@appium/types';
|
||||
import {getSchema, validate} from '../schema/schema';
|
||||
|
||||
import {formatErrors} from '../schema/format-errors';
|
||||
import {getSchema, validate} from '../schema/schema';
|
||||
import {camelCase, getPath, mapKeys, mapValues} from '../utils';
|
||||
|
||||
/**
|
||||
* A cache of the raw config file (a JSON string) at a filepath.
|
||||
@@ -99,35 +100,22 @@ export async function readConfigFile(
|
||||
export function normalizeConfig(config: AppiumConfig): NormalizedAppiumConfig {
|
||||
const schema = getSchema();
|
||||
|
||||
const isSchemaTypeObject = (
|
||||
schemaObj: SchemaObject | Record<string, unknown> | undefined,
|
||||
): boolean =>
|
||||
const isSchemaTypeObject = (schemaObj: SchemaObject | Record<string, unknown> | undefined): boolean =>
|
||||
Boolean(
|
||||
(schemaObj as SchemaObject | undefined)?.properties ||
|
||||
(schemaObj as SchemaObject | undefined)?.type === 'object',
|
||||
(schemaObj as SchemaObject | undefined)?.properties || (schemaObj as SchemaObject | undefined)?.type === 'object',
|
||||
);
|
||||
|
||||
const normalize = (rootConfig: AppiumConfig, section?: string): Record<string, unknown> => {
|
||||
const obj =
|
||||
section === undefined
|
||||
? rootConfig
|
||||
: (getPath(rootConfig, section, rootConfig) as Record<string, unknown>);
|
||||
section === undefined ? rootConfig : (getPath(rootConfig, section, rootConfig) as Record<string, unknown>);
|
||||
|
||||
const mappedObj = mapKeys(obj as Record<string, unknown>, (_v, prop) =>
|
||||
String(
|
||||
getPath(
|
||||
schema,
|
||||
`properties.server.properties.${prop}.appiumCliDest`,
|
||||
camelCase(String(prop)),
|
||||
),
|
||||
),
|
||||
String(getPath(schema, `properties.server.properties.${prop}.appiumCliDest`, camelCase(String(prop)))),
|
||||
);
|
||||
|
||||
return mapValues(mappedObj, (value, property) => {
|
||||
const nextSection = section ? `${section}.${property}` : property;
|
||||
return isSchemaTypeObject((schema as any).properties?.[property])
|
||||
? normalize(rootConfig, nextSection)
|
||||
: value;
|
||||
return isSchemaTypeObject((schema as any).properties?.[property]) ? normalize(rootConfig, nextSection) : value;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -141,10 +129,9 @@ function yamlLoader(filepath: string, content: string): unknown {
|
||||
try {
|
||||
return yaml.parse(content);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`The YAML config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`,
|
||||
{cause: e},
|
||||
);
|
||||
throw new Error(`The YAML config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`, {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,27 +145,22 @@ function jsonLoader(filepath: string, content: string): unknown {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
rawConfig.delete(filepath);
|
||||
throw new Error(
|
||||
`The JSON config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`,
|
||||
{cause: e},
|
||||
);
|
||||
throw new Error(`The JSON config at '${filepath}' cannot be loaded. Original error: ${(e as Error).message}`, {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a config file from an explicit path
|
||||
*/
|
||||
async function loadConfigFile(
|
||||
lc: LilconfigAsyncSearcher,
|
||||
filepath: string,
|
||||
): Promise<LilconfigResult> {
|
||||
async function loadConfigFile(lc: LilconfigAsyncSearcher, filepath: string): Promise<LilconfigResult> {
|
||||
try {
|
||||
// removing "await" will cause any rejection to _not_ be caught in this block!
|
||||
return await lc.load(filepath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
(err as NodeJS.ErrnoException).message =
|
||||
`Config file not found at user-provided path: ${filepath}`;
|
||||
(err as NodeJS.ErrnoException).message = `Config file not found at user-provided path: ${filepath}`;
|
||||
throw err;
|
||||
} else if (err instanceof SyntaxError) {
|
||||
// generally invalid JSON
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import {fs} from '@appium/support';
|
||||
import type {StringRecord} from '@appium/types';
|
||||
import axios from 'axios';
|
||||
|
||||
import {log as logger} from '../logger';
|
||||
|
||||
/**
|
||||
@@ -39,12 +40,7 @@ interface Grid3ProxyApiResponse {
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
export default async function registerNode(
|
||||
data: string,
|
||||
addr: string,
|
||||
port: number,
|
||||
basePath: string,
|
||||
): Promise<void>;
|
||||
export default async function registerNode(data: string, addr: string, port: number, basePath: string): Promise<void>;
|
||||
export default async function registerNode(
|
||||
data: Grid3NodeConfig,
|
||||
addr?: string,
|
||||
@@ -80,9 +76,7 @@ export default async function registerNode(
|
||||
);
|
||||
}
|
||||
if (typeof port !== 'number' || !Number.isFinite(port)) {
|
||||
throw logger.errorWithException(
|
||||
'When registering from a node config file path, port must be a finite number.',
|
||||
);
|
||||
throw logger.errorWithException('When registering from a node config file path, port must be a finite number.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +97,7 @@ export default async function registerNode(
|
||||
configHolder = JSON.parse(fileContent) as Grid3NodeConfig;
|
||||
} catch (err) {
|
||||
throw logger.errorWithException(
|
||||
`Syntax error in Selenium Grid 3 node configuration file ${configFilePath}: ` +
|
||||
(err as Error).message,
|
||||
`Syntax error in Selenium Grid 3 node configuration file ${configFilePath}: ` + (err as Error).message,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -134,29 +127,15 @@ async function registerToGrid(
|
||||
if (status !== 200) {
|
||||
throw new Error(`Request failed with code ${status}`);
|
||||
}
|
||||
logger.debug(
|
||||
`Appium successfully registered with the Selenium Grid 3 hub at ` + hubUri(hubCfg),
|
||||
);
|
||||
logger.debug(`Appium successfully registered with the Selenium Grid 3 hub at ` + hubUri(hubCfg));
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`An attempt to register with the Selenium Grid 3 hub was unsuccessful: ` +
|
||||
(err as Error).message,
|
||||
);
|
||||
logger.error(`An attempt to register with the Selenium Grid 3 hub was unsuccessful: ` + (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function postRequest(
|
||||
configHolder: Grid3NodeConfig,
|
||||
addr?: string,
|
||||
port?: number,
|
||||
basePath?: string,
|
||||
): void {
|
||||
function postRequest(configHolder: Grid3NodeConfig, addr?: string, port?: number, basePath?: string): void {
|
||||
// Move Selenium Grid 3 (flat) configuration properties into `configuration`
|
||||
if (
|
||||
configHolder != null &&
|
||||
typeof configHolder === 'object' &&
|
||||
!Object.hasOwn(configHolder, 'configuration')
|
||||
) {
|
||||
if (configHolder != null && typeof configHolder === 'object' && !Object.hasOwn(configHolder, 'configuration')) {
|
||||
const configuration: StringRecord = {};
|
||||
const holder = configHolder as StringRecord;
|
||||
for (const property of Object.keys(holder)) {
|
||||
@@ -201,11 +180,7 @@ function postRequest(
|
||||
}
|
||||
|
||||
const registerCycleInterval = cfg.registerCycle;
|
||||
if (
|
||||
registerCycleInterval === undefined ||
|
||||
isNaN(registerCycleInterval) ||
|
||||
registerCycleInterval <= 0
|
||||
) {
|
||||
if (registerCycleInterval === undefined || isNaN(registerCycleInterval) || registerCycleInterval <= 0) {
|
||||
logger.warn(
|
||||
`'registerCycle' is not a valid positive number. ` +
|
||||
`No registration request will be sent to the Selenium Grid 3 hub.`,
|
||||
@@ -215,8 +190,7 @@ function postRequest(
|
||||
// initiate a new Thread
|
||||
let first = true;
|
||||
logger.debug(
|
||||
`Starting auto-register thread for Selenium Grid 3. ` +
|
||||
`Will try to register every ${registerCycleInterval} ms.`,
|
||||
`Starting auto-register thread for Selenium Grid 3. ` + `Will try to register every ${registerCycleInterval} ms.`,
|
||||
);
|
||||
setInterval(async function registerRetry() {
|
||||
if (first) {
|
||||
@@ -231,7 +205,7 @@ function postRequest(
|
||||
|
||||
/** Query the Selenium Grid 3 hub to see if this node id is already registered. */
|
||||
async function isAlreadyRegistered(configHolder: Grid3NodeConfig): Promise<boolean | undefined> {
|
||||
//check if node is already registered
|
||||
// check if node is already registered
|
||||
const hubCfg = configHolder.configuration;
|
||||
if (!hubCfg?.id) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type {AppiumDriver} from '../appium';
|
||||
import type {
|
||||
Args,
|
||||
CliCommand,
|
||||
@@ -7,6 +6,8 @@ import type {
|
||||
CliExtensionSubcommand,
|
||||
ParsedArgs,
|
||||
} from 'appium/types';
|
||||
|
||||
import type {AppiumDriver} from '../appium';
|
||||
import type {ExtensionConfigs} from '../extension';
|
||||
|
||||
/** Empty object returned when `init` completes for non-server CLI flows (extension/setup). */
|
||||
@@ -25,7 +26,4 @@ export type InitResult<Cmd extends CliCommand = CliCommandServer> = Cmd extends
|
||||
: ExtCommandInitResult;
|
||||
|
||||
/** CLI + programmatic args before `init` narrows by `subcommand`. */
|
||||
export type PreConfigArgs = Args<
|
||||
CliCommand,
|
||||
CliExtensionSubcommand | CliCommandSetupSubcommand | void
|
||||
>;
|
||||
export type PreConfigArgs = Args<CliCommand, CliExtensionSubcommand | CliCommandSetupSubcommand | void>;
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import type {NetworkInterfaceInfo} from 'node:os';
|
||||
import {inspect as dump, type InspectOptions} from 'node:util';
|
||||
import {log as logger} from '../logger';
|
||||
|
||||
import {
|
||||
routeConfiguringFunction as makeRouter,
|
||||
normalizeBasePath,
|
||||
routeConfiguringFunction as makeRouter,
|
||||
server as baseServer,
|
||||
type ServerOpts,
|
||||
} from '@appium/base-driver';
|
||||
import {console as supportConsole, util} from '@appium/support';
|
||||
import type {AppiumServer, Driver, MethodMap, UpdateServerCallback} from '@appium/types';
|
||||
import type {Args, CliCommandServer, ParsedArgs} from 'appium/types';
|
||||
import {WebSocketServer} from 'ws';
|
||||
import type {NetworkInterfaceInfo} from 'node:os';
|
||||
|
||||
import type {AppiumDriver} from '../appium';
|
||||
import {BIDI_BASE_PATH, LONG_STACKTRACE_LIMIT} from '../constants';
|
||||
import type {DriverNameMap, PluginNameMap} from '../extension';
|
||||
import {APPIUM_VER, getBuildInfo, getGitRev, updateBuildInfo} from '../helpers/build';
|
||||
import {fetchInterfaces, isBroadcastIp, V4_BROADCAST_IP} from '../helpers/network';
|
||||
import {log as logger} from '../logger';
|
||||
import {validate as validateSchema} from '../schema/schema';
|
||||
import {checkNodeOk, requireDir} from './node-helpers';
|
||||
import {getNonDefaultServerArgs} from './startup-config';
|
||||
import {validate as validateSchema} from '../schema/schema';
|
||||
import {fetchInterfaces, V4_BROADCAST_IP, isBroadcastIp} from '../helpers/network';
|
||||
import {LONG_STACKTRACE_LIMIT, BIDI_BASE_PATH} from '../constants';
|
||||
import type {Args, ParsedArgs, CliCommandServer} from 'appium/types';
|
||||
import type {DriverNameMap, PluginNameMap} from '../extension';
|
||||
|
||||
const isStdoutTTY = process.stdout.isTTY;
|
||||
|
||||
@@ -88,10 +90,7 @@ export function logServerAddress(url: string): void {
|
||||
* @param args - Parsed server CLI args
|
||||
* @param throwInsteadOfExit - When true, rethrows failures instead of calling `process.exit(1)`
|
||||
*/
|
||||
export async function preflightChecks(
|
||||
args: ParsedArgs<CliCommandServer>,
|
||||
throwInsteadOfExit = false,
|
||||
): Promise<void> {
|
||||
export async function preflightChecks(args: ParsedArgs<CliCommandServer>, throwInsteadOfExit = false): Promise<void> {
|
||||
try {
|
||||
checkNodeOk();
|
||||
if (args.longStacktrace) {
|
||||
@@ -141,10 +140,7 @@ export async function logStartupInfo(args: ParsedArgs<CliCommandServer>): Promis
|
||||
/**
|
||||
* Collects `updateServer` hooks from active driver and plugin classes for HTTP server customization.
|
||||
*/
|
||||
export function getServerUpdaters(
|
||||
driverClasses: DriverNameMap,
|
||||
pluginClasses: PluginNameMap,
|
||||
): UpdateServerCallback[] {
|
||||
export function getServerUpdaters(driverClasses: DriverNameMap, pluginClasses: PluginNameMap): UpdateServerCallback[] {
|
||||
return [...driverClasses.keys(), ...pluginClasses.keys()]
|
||||
.map((klass) => klass.updateServer)
|
||||
.filter(Boolean) as UpdateServerCallback[];
|
||||
@@ -153,10 +149,7 @@ export function getServerUpdaters(
|
||||
/**
|
||||
* Merges `newMethodMap` contributions from all active drivers and plugins into one method map.
|
||||
*/
|
||||
export function getExtraMethodMap(
|
||||
driverClasses: DriverNameMap,
|
||||
pluginClasses: PluginNameMap,
|
||||
): MethodMap<Driver> {
|
||||
export function getExtraMethodMap(driverClasses: DriverNameMap, pluginClasses: PluginNameMap): MethodMap<Driver> {
|
||||
return [...driverClasses.keys(), ...pluginClasses.keys()].reduce<MethodMap<Driver>>(
|
||||
(map, klass) => ({
|
||||
...map,
|
||||
@@ -218,12 +211,9 @@ function logNonDefaultArgsWarning(args: Args): void {
|
||||
inspect(args);
|
||||
}
|
||||
|
||||
function logDefaultCapabilitiesWarning(
|
||||
caps: ParsedArgs<CliCommandServer>['defaultCapabilities'],
|
||||
): void {
|
||||
function logDefaultCapabilitiesWarning(caps: ParsedArgs<CliCommandServer>['defaultCapabilities']): void {
|
||||
logger.info(
|
||||
'Default capabilities, which will be added to each request ' +
|
||||
'unless overridden by desired capabilities:',
|
||||
'Default capabilities, which will be added to each request ' + 'unless overridden by desired capabilities:',
|
||||
);
|
||||
inspect(caps);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import {fs, npm, system} from '@appium/support';
|
||||
import * as semver from 'semver';
|
||||
import {system, fs, npm} from '@appium/support';
|
||||
|
||||
import {getBuildInfo, updateBuildInfo} from '../helpers/build';
|
||||
import {appiumPackageRoot, npmPackage} from '../utils';
|
||||
import {updateBuildInfo, getBuildInfo} from '../helpers/build';
|
||||
|
||||
const MIN_NODE_VERSION = (npmPackage.engines as Record<string, string>).node;
|
||||
|
||||
@@ -24,9 +26,7 @@ interface DebugInfoInput {
|
||||
export function checkNodeOk(): void {
|
||||
const version = getNodeVersion();
|
||||
if (!semver.satisfies(version, MIN_NODE_VERSION)) {
|
||||
throw new Error(
|
||||
`Node version must be at least ${MIN_NODE_VERSION}; current is ${version.version}`,
|
||||
);
|
||||
throw new Error(`Node version must be at least ${MIN_NODE_VERSION}; current is ${version.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ export function adjustNodePath(): void {
|
||||
if (refreshRequirePaths()) {
|
||||
process.env.APPIUM_OMIT_PEER_DEPS = '1';
|
||||
} else {
|
||||
process.env.NODE_PATH = nodePathParts
|
||||
.filter((p) => p !== appiumModuleSearchRoot)
|
||||
.join(path.delimiter);
|
||||
process.env.NODE_PATH = nodePathParts.filter((p) => p !== appiumModuleSearchRoot).join(path.delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,17 +75,12 @@ export function adjustNodePath(): void {
|
||||
* Prints JSON debug info (OS, Node/npm, Appium build, installed drivers/plugins) to stdout.
|
||||
* Input combines extension config snapshots with the resolved Appium home path.
|
||||
*/
|
||||
export async function showDebugInfo({
|
||||
driverConfig,
|
||||
pluginConfig,
|
||||
appiumHome,
|
||||
}: DebugInfoInput): Promise<void> {
|
||||
export async function showDebugInfo({driverConfig, pluginConfig, appiumHome}: DebugInfoInput): Promise<void> {
|
||||
const getNpmVersion = async (): Promise<string> => {
|
||||
const {stdout} = await npm.exec('--version', [], {cwd: process.cwd()});
|
||||
return stdout.trim();
|
||||
};
|
||||
const findNpmLocation = async (): Promise<string> =>
|
||||
await fs.which(system.isWindows() ? 'npm.cmd' : 'npm');
|
||||
const findNpmLocation = async (): Promise<string> => await fs.which(system.isWindows() ? 'npm.cmd' : 'npm');
|
||||
|
||||
const [npmVersion, npmLocation] = await Promise.all([
|
||||
...[getNpmVersion, findNpmLocation].map((f) => getSafeResult(f, 'unknown')),
|
||||
@@ -131,11 +124,7 @@ export async function showDebugInfo({
|
||||
*
|
||||
* @throws {Error}
|
||||
*/
|
||||
export async function requireDir(
|
||||
root: string,
|
||||
requireWriteable = true,
|
||||
displayName = 'folder path',
|
||||
): Promise<void> {
|
||||
export async function requireDir(root: string, requireWriteable = true, displayName = 'folder path'): Promise<void> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(root);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
import {util} from '@appium/support';
|
||||
import {getDefaultsForSchema, getAllArgSpecs} from '../schema/schema';
|
||||
import type {Args} from 'appium/types';
|
||||
import type {ReadConfigFileResult} from './config-file';
|
||||
|
||||
import {getAllArgSpecs, getDefaultsForSchema} from '../schema/schema';
|
||||
import {difference, getPath, pickBy, setPath} from '../utils';
|
||||
import type {ReadConfigFileResult} from './config-file';
|
||||
|
||||
interface FlattenedArg {
|
||||
value: unknown;
|
||||
@@ -22,16 +23,13 @@ export function getNonDefaultServerArgs(parsedArgs: Args): Args {
|
||||
*/
|
||||
const flatten = (args: Args): Record<string, FlattenedArg> => {
|
||||
const argSpecs = getAllArgSpecs();
|
||||
const flattened = [...argSpecs.values()].reduce<Record<string, FlattenedArg>>(
|
||||
(acc, argSpec: {dest: string}) => {
|
||||
const value = getPath(args, argSpec.dest);
|
||||
if (value !== undefined) {
|
||||
acc[argSpec.dest] = {value, argSpec};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const flattened = [...argSpecs.values()].reduce<Record<string, FlattenedArg>>((acc, argSpec: {dest: string}) => {
|
||||
const value = getPath(args, argSpec.dest);
|
||||
if (value !== undefined) {
|
||||
acc[argSpec.dest] = {value, argSpec};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return flattened;
|
||||
};
|
||||
@@ -39,8 +37,7 @@ export function getNonDefaultServerArgs(parsedArgs: Args): Args {
|
||||
const args = flatten(parsedArgs);
|
||||
|
||||
// hopefully these function names are descriptive enough
|
||||
const typesDiffer = (dest: string): boolean =>
|
||||
typeof args[dest].value !== typeof defaultsFromSchema[dest];
|
||||
const typesDiffer = (dest: string): boolean => typeof args[dest].value !== typeof defaultsFromSchema[dest];
|
||||
|
||||
const defaultValueIsArray = (dest: string): boolean => Array.isArray(defaultsFromSchema[dest]);
|
||||
|
||||
@@ -53,11 +50,9 @@ export function getNonDefaultServerArgs(parsedArgs: Args): Args {
|
||||
|
||||
const defaultIsDefined = (dest: string): boolean => defaultsFromSchema[dest] !== undefined;
|
||||
|
||||
const argValueNotArrayOrArraysDiffer = (dest: string) =>
|
||||
!argsValueIsArray(dest) || arraysDiffer(dest);
|
||||
const argValueNotArrayOrArraysDiffer = (dest: string) => !argsValueIsArray(dest) || arraysDiffer(dest);
|
||||
|
||||
const defaultValueNotArrayAndValuesDiffer = (dest: string) =>
|
||||
!defaultValueIsArray(dest) && valuesDiffer(dest);
|
||||
const defaultValueNotArrayAndValuesDiffer = (dest: string) => !defaultValueIsArray(dest) && valuesDiffer(dest);
|
||||
|
||||
/**
|
||||
* This used to be a hideous conditional, but it's broken up into a hideous function instead.
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import {util} from '@appium/support';
|
||||
import type {ArgumentOptions} from 'argparse';
|
||||
import type {ExtensionType} from '@appium/types';
|
||||
import type {CliExtensionSubcommand} from 'appium/types';
|
||||
import type {ArgumentOptions} from 'argparse';
|
||||
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
PLUGIN_TYPE,
|
||||
EXT_SUBCOMMAND_DOCTOR,
|
||||
EXT_SUBCOMMAND_INSTALL,
|
||||
EXT_SUBCOMMAND_LIST,
|
||||
EXT_SUBCOMMAND_RUN,
|
||||
EXT_SUBCOMMAND_UNINSTALL,
|
||||
EXT_SUBCOMMAND_UPDATE,
|
||||
EXT_SUBCOMMAND_DOCTOR,
|
||||
PLUGIN_TYPE,
|
||||
} from '../constants';
|
||||
import {INSTALL_TYPES} from '../extension/extension-config';
|
||||
import {toParserArgs} from '../schema/cli-args';
|
||||
const DRIVER_EXAMPLE = 'xcuitest';
|
||||
const PLUGIN_EXAMPLE = 'images';
|
||||
|
||||
export type ArgumentDefinitions = Map<
|
||||
[name: string] | [name: string, alias: string],
|
||||
ArgumentOptions
|
||||
>;
|
||||
export type ArgumentDefinitions = Map<[name: string] | [name: string, alias: string], ArgumentOptions>;
|
||||
|
||||
/**
|
||||
* This is necessary because we pass the array into `argparse`. `argparse` is bad and mutates things. We don't want that.
|
||||
@@ -53,10 +51,7 @@ export const getExtensionArgs = util.memoize(function getExtensionArgs(): Record
|
||||
ExtensionType,
|
||||
Record<CliExtensionSubcommand, ArgumentDefinitions>
|
||||
> {
|
||||
const extensionArgs = {} as Record<
|
||||
ExtensionType,
|
||||
Record<CliExtensionSubcommand, ArgumentDefinitions>
|
||||
>;
|
||||
const extensionArgs = {} as Record<ExtensionType, Record<CliExtensionSubcommand, ArgumentDefinitions>>;
|
||||
for (const type of EXTENSION_TYPES) {
|
||||
extensionArgs[type] = {
|
||||
[EXT_SUBCOMMAND_LIST]: makeListArgs(type),
|
||||
@@ -130,8 +125,7 @@ function makeInstallArgs(type: ExtensionType): ArgumentDefinitions {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to install, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
`Name of the ${type} to install, for example: ` + (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -152,9 +146,7 @@ function makeInstallArgs(type: ExtensionType): ArgumentDefinitions {
|
||||
required: false,
|
||||
default: null,
|
||||
type: 'str',
|
||||
help:
|
||||
`The Node.js package name of the ${type}. ` +
|
||||
`Required if "source" is set to "git" or "github".`,
|
||||
help: `The Node.js package name of the ${type}. ` + `Required if "source" is set to "git" or "github".`,
|
||||
dest: 'packageName',
|
||||
},
|
||||
],
|
||||
@@ -172,8 +164,7 @@ function makeUninstallArgs(type: ExtensionType): ArgumentDefinitions {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to uninstall, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
`Name of the ${type} to uninstall, for example: ` + (type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {console, util} from '@appium/support';
|
||||
import type {ExtMetadata, ExtRecord, InstallType} from 'appium/types';
|
||||
import ExtensionCliCommand from './extension-command';
|
||||
|
||||
import {KNOWN_DRIVERS} from '../constants';
|
||||
import {ExtensionCliCommand} from './extension-command';
|
||||
import type {
|
||||
ExtensionArgs,
|
||||
ExtensionCommandOptions,
|
||||
@@ -8,7 +10,6 @@ import type {
|
||||
PostInstallText,
|
||||
RunOutput,
|
||||
} from './extension-command';
|
||||
import {KNOWN_DRIVERS} from '../constants';
|
||||
const REQ_DRIVER_FIELDS = ['driverName', 'automationName', 'platformNames', 'mainClass'];
|
||||
type DriverInstallOpts = {driver: string; installType: InstallType; packageName?: string};
|
||||
type DriverUninstallOpts = {driver: string};
|
||||
@@ -27,11 +28,7 @@ export default class DriverCliCommand extends ExtensionCliCommand<'driver'> {
|
||||
*
|
||||
* @param opts - install options
|
||||
*/
|
||||
async install({
|
||||
driver,
|
||||
installType,
|
||||
packageName,
|
||||
}: DriverInstallOpts): Promise<ExtRecord<'driver'>> {
|
||||
async install({driver, installType, packageName}: DriverInstallOpts): Promise<ExtRecord<'driver'>> {
|
||||
return await super._install({
|
||||
installSpec: driver,
|
||||
installType,
|
||||
@@ -108,13 +105,9 @@ export default class DriverCliCommand extends ExtensionCliCommand<'driver'> {
|
||||
* @param driverMetadata - `appium` metadata from extension package
|
||||
* @param installSpec - install spec from CLI
|
||||
*/
|
||||
override validateExtensionFields(
|
||||
driverMetadata: ExtMetadata<'driver'>,
|
||||
installSpec: string,
|
||||
): void {
|
||||
override validateExtensionFields(driverMetadata: ExtMetadata<'driver'>, installSpec: string): void {
|
||||
const missingFields = REQ_DRIVER_FIELDS.reduce<string[]>(
|
||||
(acc, field) =>
|
||||
driverMetadata[field as keyof typeof driverMetadata] ? acc : [...acc, field],
|
||||
(acc, field) => (driverMetadata[field as keyof typeof driverMetadata] ? acc : [...acc, field]),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import {asyncfilter, asyncmap} from 'asyncbox';
|
||||
import {spawn} from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
import {inspect} from 'node:util';
|
||||
|
||||
import {console, env, fs, npm, system, util} from '@appium/support';
|
||||
import type {AppiumLogger, ExtensionType, IDoctorCheck} from '@appium/types';
|
||||
import type {
|
||||
@@ -10,23 +13,22 @@ import type {
|
||||
ExtRecord as AppiumExtRecord,
|
||||
InstallType,
|
||||
} from 'appium/types';
|
||||
import {asyncfilter, asyncmap} from 'asyncbox';
|
||||
import * as semver from 'semver';
|
||||
import {SubProcess} from 'teen_process';
|
||||
import type {PackageJson} from 'type-fest';
|
||||
|
||||
import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
|
||||
import type {ExtensionConfig as BaseExtensionConfig} from '../extension/extension-config';
|
||||
import {spinWith, RingBuffer} from './utils';
|
||||
import {
|
||||
INSTALL_TYPE_NPM,
|
||||
INSTALL_TYPE_DEV,
|
||||
INSTALL_TYPE_GIT,
|
||||
INSTALL_TYPE_GITHUB,
|
||||
INSTALL_TYPE_LOCAL,
|
||||
INSTALL_TYPE_DEV,
|
||||
INSTALL_TYPE_NPM,
|
||||
} from '../extension/extension-config';
|
||||
import {SubProcess} from 'teen_process';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {inspect} from 'node:util';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
|
||||
import {appiumPackageRoot, compact, npmPackage, packageDidChange} from '../utils';
|
||||
import * as semver from 'semver';
|
||||
import {RingBuffer, spinWith} from './utils';
|
||||
|
||||
const UPDATE_ALL = 'installed';
|
||||
const MAX_CONCURRENT_REPO_FETCHES = 5;
|
||||
@@ -38,17 +40,14 @@ export type ExtensionCommandOptions<ExtType extends ExtensionType = ExtensionTyp
|
||||
config: ExtensionConfig<ExtType>;
|
||||
json: boolean;
|
||||
};
|
||||
export type ExtensionConfig<ExtType extends ExtensionType = ExtensionType> =
|
||||
BaseExtensionConfig<ExtType>;
|
||||
export type ExtensionConfig<ExtType extends ExtensionType = ExtensionType> = BaseExtensionConfig<ExtType>;
|
||||
|
||||
export type ExtRecord<ExtType extends ExtensionType = ExtensionType> = AppiumExtRecord<ExtType>;
|
||||
|
||||
export type ExtMetadata<ExtType extends ExtensionType = ExtensionType> = AppiumExtMetadata<ExtType>;
|
||||
export type ExtManifest<ExtType extends ExtensionType = ExtensionType> = AppiumExtManifest<ExtType>;
|
||||
export type ExtPackageJson<ExtType extends ExtensionType = ExtensionType> =
|
||||
AppiumExtPackageJson<ExtType>;
|
||||
export type ExtInstallReceipt<ExtType extends ExtensionType = ExtensionType> =
|
||||
AppiumExtInstallReceipt<ExtType>;
|
||||
export type ExtPackageJson<ExtType extends ExtensionType = ExtensionType> = AppiumExtPackageJson<ExtType>;
|
||||
export type ExtInstallReceipt<ExtType extends ExtensionType = ExtensionType> = AppiumExtInstallReceipt<ExtType>;
|
||||
/**
|
||||
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
|
||||
*/
|
||||
@@ -64,21 +63,16 @@ export type ExtensionListMetadata = {
|
||||
/**
|
||||
* Possible return value for {@linkcode ExtensionCliCommand.list}
|
||||
*/
|
||||
export type ExtensionListData<ExtType extends ExtensionType = ExtensionType> = Partial<
|
||||
ExtManifest<ExtType>
|
||||
> &
|
||||
export type ExtensionListData<ExtType extends ExtensionType = ExtensionType> = Partial<ExtManifest<ExtType>> &
|
||||
Partial<ExtensionListMetadata>;
|
||||
|
||||
export type InstalledExtensionListData<ExtType extends ExtensionType = ExtensionType> =
|
||||
ExtManifest<ExtType> & ExtensionListMetadata;
|
||||
export type InstalledExtensionListData<ExtType extends ExtensionType = ExtensionType> = ExtManifest<ExtType> &
|
||||
ExtensionListMetadata;
|
||||
|
||||
/**
|
||||
* Return value of {@linkcode ExtensionCliCommand.list}.
|
||||
*/
|
||||
export type ExtensionList<ExtType extends ExtensionType = ExtensionType> = Record<
|
||||
string,
|
||||
ExtensionListData<ExtType>
|
||||
>;
|
||||
export type ExtensionList<ExtType extends ExtensionType = ExtensionType> = Record<string, ExtensionListData<ExtType>>;
|
||||
|
||||
/**
|
||||
* Return value of {@linkcode ExtensionCliCommand._run}
|
||||
@@ -171,7 +165,7 @@ class NotUpdatableError extends Error {}
|
||||
|
||||
class NoUpdatesAvailableError extends Error {}
|
||||
|
||||
abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType> {
|
||||
export abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType> {
|
||||
/**
|
||||
* This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
|
||||
*/
|
||||
@@ -264,13 +258,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Build the initial list data structure from installed and known extensions
|
||||
*
|
||||
*/
|
||||
protected async _install({
|
||||
installSpec,
|
||||
installType,
|
||||
packageName,
|
||||
}: InstallOpts): Promise<Record<string, any>> {
|
||||
protected async _install({installSpec, installType, packageName}: InstallOpts): Promise<Record<string, any>> {
|
||||
if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
|
||||
throw this._createFatalError(`When using --source=${installType}, cannot also use --package`);
|
||||
}
|
||||
@@ -292,8 +281,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
if (installType === INSTALL_TYPE_GITHUB) {
|
||||
if (installSpec.split('/').length !== 2) {
|
||||
throw this._createFatalError(
|
||||
`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
|
||||
'it should be of the form <org>/<repo>',
|
||||
`Github ${this.type} spec ${installSpec} appeared to be invalid; ` + 'it should be of the form <org>/<repo>',
|
||||
);
|
||||
}
|
||||
installViaNpmOpts = {
|
||||
@@ -397,10 +385,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
]);
|
||||
const errorMap = new Map([[extName, errors]]);
|
||||
const warningMap = new Map([[extName, warnings]]);
|
||||
const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(
|
||||
errorMap,
|
||||
warningMap,
|
||||
);
|
||||
const {errorSummaries, warningSummaries} = this.config.getValidationResultSummaries(errorMap, warningMap);
|
||||
|
||||
if (!util.isEmpty(errorSummaries)) {
|
||||
throw this._createFatalError(errorSummaries.join('\n'));
|
||||
@@ -419,9 +404,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
}
|
||||
|
||||
// log info for the user
|
||||
this.log.info(
|
||||
this.getPostInstallText({extName, extData: receipt as unknown as ExtInstallReceipt<ExtType>}),
|
||||
);
|
||||
this.log.info(this.getPostInstallText({extName, extData: receipt as unknown as ExtInstallReceipt<ExtType>}));
|
||||
|
||||
return this.config.installedExtensions;
|
||||
}
|
||||
@@ -437,9 +420,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
*/
|
||||
protected async _uninstall({installSpec}: UninstallOpts): Promise<Record<string, any>> {
|
||||
if (!this.config.isInstalled(installSpec)) {
|
||||
throw this._createFatalError(
|
||||
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`,
|
||||
);
|
||||
throw this._createFatalError(`Can't uninstall ${this.type} '${installSpec}'; it is not installed`);
|
||||
}
|
||||
const extRecord = this.config.installedExtensions[installSpec];
|
||||
if (extRecord.installType === INSTALL_TYPE_DEV) {
|
||||
@@ -451,30 +432,20 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
await npm.uninstallPackage(this.config.appiumHome, pkgName);
|
||||
});
|
||||
await this.config.removeExtension(installSpec);
|
||||
this.log.ok(
|
||||
console.styleText('green', `Successfully uninstalled ${this.type} '${installSpec}'`),
|
||||
);
|
||||
this.log.ok(console.styleText('green', `Successfully uninstalled ${this.type} '${installSpec}'`));
|
||||
return this.config.installedExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to update one or more drivers using NPM
|
||||
*
|
||||
*/
|
||||
protected async _update({
|
||||
installSpec,
|
||||
unsafe,
|
||||
}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
|
||||
protected async _update({installSpec, unsafe}: ExtensionUpdateOpts): Promise<ExtensionUpdateResult> {
|
||||
const shouldUpdateAll = installSpec === UPDATE_ALL;
|
||||
// if we're specifically requesting an update for an extension, make sure it's installed
|
||||
if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
|
||||
throw this._createFatalError(
|
||||
`The ${this.type} "${installSpec}" was not installed, so can't be updated`,
|
||||
);
|
||||
throw this._createFatalError(`The ${this.type} "${installSpec}" was not installed, so can't be updated`);
|
||||
}
|
||||
const extsToUpdate = shouldUpdateAll
|
||||
? Object.keys(this.config.installedExtensions)
|
||||
: [installSpec];
|
||||
const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [installSpec];
|
||||
|
||||
// 'errors' will have ext names as keys and error objects as values
|
||||
const errors: Record<string, Error> = {};
|
||||
@@ -534,18 +505,13 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
this.log.info('Update report:');
|
||||
|
||||
for (const [e, update] of Object.entries(updates)) {
|
||||
this.log.ok(
|
||||
console.styleText('green', ` - ${this.type} ${e} updated: ${update.from} => ${update.to}`),
|
||||
);
|
||||
this.log.ok(console.styleText('green', ` - ${this.type} ${e} updated: ${update.from} => ${update.to}`));
|
||||
}
|
||||
|
||||
for (const [e, err] of Object.entries(errors)) {
|
||||
if (err instanceof NotUpdatableError) {
|
||||
this.log.warn(
|
||||
console.styleText(
|
||||
'yellow',
|
||||
` - '${e}' was not installed via npm, so we could not check for updates`,
|
||||
),
|
||||
console.styleText('yellow', ` - '${e}' was not installed via npm, so we could not check for updates`),
|
||||
);
|
||||
} else if (err instanceof NoUpdatesAvailableError) {
|
||||
this.log.info(console.styleText('yellow', ` - '${e}' had no updates available`));
|
||||
@@ -569,11 +535,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
// install type
|
||||
const {version, pkgName} = this.config.installedExtensions[ext];
|
||||
let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
|
||||
let safeUpdate = await npm.getLatestSafeUpgradeVersion(
|
||||
this.config.appiumHome,
|
||||
pkgName,
|
||||
version,
|
||||
);
|
||||
let safeUpdate = await npm.getLatestSafeUpgradeVersion(this.config.appiumHome, pkgName, version);
|
||||
if (unsafeUpdate !== null && !util.compareVersions(unsafeUpdate, '>', version)) {
|
||||
// the latest version is not greater than the current version, so there's no possible update
|
||||
unsafeUpdate = null;
|
||||
@@ -605,18 +567,14 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
const moduleRoot = this.config.getInstallPath(installSpec);
|
||||
const packageJsonPath = path.join(moduleRoot, 'package.json');
|
||||
if (!(await fs.exists(packageJsonPath))) {
|
||||
throw this._createFatalError(
|
||||
`No package.json could be found for "${installSpec}" ${this.type}`,
|
||||
);
|
||||
throw this._createFatalError(`No package.json could be found for "${installSpec}" ${this.type}`);
|
||||
}
|
||||
let doctorSpec: {checks: string[]} | undefined;
|
||||
try {
|
||||
doctorSpec = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')).appium?.doctor;
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
throw this._createFatalError(
|
||||
`The manifest at '${packageJsonPath}' cannot be parsed: ${message}`,
|
||||
);
|
||||
throw this._createFatalError(`The manifest at '${packageJsonPath}' cannot be parsed: ${message}`);
|
||||
}
|
||||
if (!doctorSpec) {
|
||||
this.log.info(`The ${this.type} "${installSpec}" does not export any doctor checks`);
|
||||
@@ -631,7 +589,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
const paths: string[] = doctorSpec.checks
|
||||
.map((p) => {
|
||||
const scriptPath = path.resolve(moduleRoot, p);
|
||||
if (!path.normalize(scriptPath).startsWith(path.normalize(moduleRoot))) {
|
||||
if (!util.isSubPath(scriptPath, moduleRoot)) {
|
||||
this.log.error(
|
||||
`The doctor check script '${p}' from the package manifest '${packageJsonPath}' must be located ` +
|
||||
`in the '${moduleRoot}' root folder. It will be skipped`,
|
||||
@@ -668,8 +626,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
return 0;
|
||||
}
|
||||
this.log.debug(
|
||||
`Running ${util.pluralize('doctor check', checks.length, true)} ` +
|
||||
`for the "${installSpec}" ${this.type}`,
|
||||
`Running ${util.pluralize('doctor check', checks.length, true)} ` + `for the "${installSpec}" ${this.type}`,
|
||||
);
|
||||
const exitCode = await new Doctor(checks).run();
|
||||
if (exitCode !== DOCTOR_EXIT_CODE.SUCCESS) {
|
||||
@@ -685,7 +642,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
* underneath the `appium` field in its `package.json`, if the
|
||||
* `scripts` field is not a plain object, or if the `scriptName` is
|
||||
* not found within `scripts` object.
|
||||
*
|
||||
*/
|
||||
protected async _run({
|
||||
installSpec,
|
||||
@@ -710,18 +666,13 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
const extScripts = extConfig.scripts;
|
||||
|
||||
if (!extScripts || !util.isPlainObject(extScripts)) {
|
||||
throw this._createFatalError(
|
||||
`The ${this.type} named '${installSpec}' "scripts" field must be a plain object`,
|
||||
);
|
||||
throw this._createFatalError(`The ${this.type} named '${installSpec}' "scripts" field must be a plain object`);
|
||||
}
|
||||
|
||||
if (!scriptName) {
|
||||
const allScripts = Object.entries(extScripts as Record<string, string>);
|
||||
const root = this.config.getInstallPath(installSpec);
|
||||
const existingScripts = await asyncfilter(
|
||||
allScripts,
|
||||
async ([, p]) => await fs.exists(path.join(root, p)),
|
||||
);
|
||||
const existingScripts = await asyncfilter(allScripts, async ([, p]) => await fs.exists(path.join(root, p)));
|
||||
if (util.isEmpty(existingScripts)) {
|
||||
this.log.info(`The ${this.type} named '${installSpec}' does not contain any scripts`);
|
||||
} else {
|
||||
@@ -743,11 +694,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
const scriptPath = extScripts[scriptName];
|
||||
const moduleRoot = this.config.getInstallPath(installSpec);
|
||||
const normalizedScriptPath = path.normalize(path.resolve(moduleRoot, scriptPath));
|
||||
if (!normalizedScriptPath.startsWith(path.normalize(moduleRoot))) {
|
||||
throw this._createFatalError(
|
||||
`The '${scriptPath}' script must be located in the '${moduleRoot}' folder`,
|
||||
);
|
||||
if (!util.isSubPath(path.resolve(moduleRoot, scriptPath), moduleRoot)) {
|
||||
throw this._createFatalError(`The '${scriptPath}' script must be located in the '${moduleRoot}' folder`);
|
||||
}
|
||||
|
||||
if (bufferOutput) {
|
||||
@@ -824,7 +772,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Install an extension via NPM
|
||||
*
|
||||
*/
|
||||
private async installViaNpm({
|
||||
installSpec,
|
||||
@@ -837,8 +784,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
// the string used for installation is either <name>@<ver> in the case of a standard NPM
|
||||
// package, or whatever the user sent in otherwise.
|
||||
const installStr =
|
||||
installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
|
||||
const installStr = installType === INSTALL_TYPE_NPM ? `${pkgName}${pkgVer ? `@${pkgVer}` : ''}` : installSpec;
|
||||
const appiumHome = this.config.appiumHome;
|
||||
try {
|
||||
const {pkg, installPath} = await spinWith(
|
||||
@@ -900,12 +846,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
* @param args - Extra args for script
|
||||
* @param opts - Options
|
||||
*/
|
||||
private _runUnbuffered(
|
||||
cwd: string,
|
||||
script: string,
|
||||
args: string[] = [],
|
||||
opts: Record<string, any> = {},
|
||||
) {
|
||||
private _runUnbuffered(cwd: string, script: string, args: string[] = [], opts: Record<string, any> = {}) {
|
||||
return spawn(process.execPath, [script, ...args], {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
@@ -915,7 +856,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Once a package is installed on-disk, this gathers some necessary metadata for validation.
|
||||
*
|
||||
*/
|
||||
private getInstallationReceipt({
|
||||
pkg,
|
||||
@@ -958,9 +898,7 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
const {appium, name, version} = pkg;
|
||||
|
||||
const createMissingFieldError = (field: string): ReferenceError =>
|
||||
new ReferenceError(
|
||||
`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``,
|
||||
);
|
||||
new ReferenceError(`${this.type} "${installSpec}" invalid; missing a \`${field}\` field of its \`package.json\``);
|
||||
|
||||
if (!name) {
|
||||
throw createMissingFieldError('name');
|
||||
@@ -979,13 +917,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Check for available updates for installed extensions
|
||||
*
|
||||
*/
|
||||
private async _checkForUpdates(
|
||||
listData: ExtensionList,
|
||||
showUpdates: boolean,
|
||||
lsMsg: string,
|
||||
): Promise<void> {
|
||||
private async _checkForUpdates(listData: ExtensionList, showUpdates: boolean, lsMsg: string): Promise<void> {
|
||||
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
||||
// We'd like to still show lsMsg even if showUpdates is false
|
||||
if (!showUpdates) {
|
||||
@@ -1016,7 +949,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Add repository URLs to list data for all extensions
|
||||
*
|
||||
*/
|
||||
private async _addRepositoryUrlsToListData(listData: ExtensionList): Promise<void> {
|
||||
await spinWith(this.isJsonOutput, 'Fetching repository information', async () => {
|
||||
@@ -1035,12 +967,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Display normal formatted output
|
||||
*
|
||||
*/
|
||||
private async _displayNormalListOutput(
|
||||
listData: ExtensionList,
|
||||
showUpdates: boolean,
|
||||
): Promise<ExtensionList> {
|
||||
private async _displayNormalListOutput(listData: ExtensionList, showUpdates: boolean): Promise<ExtensionList> {
|
||||
for (const [name, data] of Object.entries(listData)) {
|
||||
const line = await this._formatExtensionLine(name, data, showUpdates);
|
||||
this.log.log(line);
|
||||
@@ -1051,13 +979,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Format a single extension line for display
|
||||
*
|
||||
*/
|
||||
private async _formatExtensionLine(
|
||||
name: string,
|
||||
data: ExtensionListData,
|
||||
showUpdates: boolean,
|
||||
): Promise<string> {
|
||||
private async _formatExtensionLine(name: string, data: ExtensionListData, showUpdates: boolean): Promise<string> {
|
||||
if (data.installed) {
|
||||
const installTxt = this._formatInstallText(data);
|
||||
const updateTxt = showUpdates ? this._formatUpdateText(data) : '';
|
||||
@@ -1069,7 +992,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Format installation status text
|
||||
*
|
||||
*/
|
||||
private _formatInstallText(data: ExtensionListData): string {
|
||||
const {installType, installSpec, version} = data;
|
||||
@@ -1094,12 +1016,14 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
const installLabelStart = console.styleText('green', '[installed ');
|
||||
const installLabelEnd = console.styleText('green', ']');
|
||||
const installTypeText = isTypeTextStyled ? typeTxt : console.styleText('green', typeTxt);
|
||||
return `${console.styleText('yellow', `@${String(version)}`)} ${installLabelStart}${installTypeText}${installLabelEnd}`;
|
||||
return `${console.styleText(
|
||||
'yellow',
|
||||
`@${String(version)}`,
|
||||
)} ${installLabelStart}${installTypeText}${installLabelEnd}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format update information text
|
||||
*
|
||||
*/
|
||||
private _formatUpdateText(data: ExtensionListData): string {
|
||||
const {updateVersion, unsafeUpdateVersion, upToDate, updateError} = data;
|
||||
@@ -1121,7 +1045,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Get repository URL from package data
|
||||
*
|
||||
*/
|
||||
private async _getRepositoryUrl(data: ExtensionListData): Promise<string | null> {
|
||||
if (data.installed && data.installPath) {
|
||||
@@ -1135,7 +1058,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Get repository URL from installed extension's package.json
|
||||
*
|
||||
*/
|
||||
private async _getRepositoryUrlFromInstalled(data: ExtensionListData): Promise<string | null> {
|
||||
try {
|
||||
@@ -1159,7 +1081,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Get repository URL from npm for a package name
|
||||
*
|
||||
*/
|
||||
private async _getRepositoryUrlFromNpm(pkgName: string): Promise<string | null> {
|
||||
try {
|
||||
@@ -1181,7 +1102,6 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
|
||||
/**
|
||||
* Checks whether the given extension is compatible with the currently installed server
|
||||
*
|
||||
*/
|
||||
private async _checkInstallCompatibility({
|
||||
installSpec,
|
||||
@@ -1194,15 +1114,8 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
}
|
||||
|
||||
await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
|
||||
const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(
|
||||
pkgName,
|
||||
pkgVer,
|
||||
);
|
||||
if (
|
||||
serverVersion &&
|
||||
extVersionRequirement &&
|
||||
!semver.satisfies(serverVersion, extVersionRequirement)
|
||||
) {
|
||||
const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(pkgName, pkgVer);
|
||||
if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
|
||||
throw this._createFatalError(
|
||||
`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
|
||||
`does not meet the currently installed one (${serverVersion}). Please install ` +
|
||||
@@ -1220,15 +1133,11 @@ abstract class ExtensionCliCommand<ExtType extends ExtensionType = ExtensionType
|
||||
* @param extMetadata - the data in the "appium" field of `package.json` for an extension
|
||||
* @param installSpec - Extension name/spec
|
||||
*/
|
||||
protected abstract validateExtensionFields(
|
||||
extMetadata: ExtMetadata<ExtType>,
|
||||
installSpec: string,
|
||||
): void;
|
||||
protected abstract validateExtensionFields(extMetadata: ExtMetadata<ExtType>, installSpec: string): void;
|
||||
|
||||
/**
|
||||
* Get the text which should be displayed to the user after an extension has been installed. This
|
||||
* is designed to be overridden by drivers/plugins with their own particular text.
|
||||
*
|
||||
*/
|
||||
protected abstract getPostInstallText(args: ExtensionArgs<ExtType>): PostInstallText;
|
||||
}
|
||||
@@ -1271,18 +1180,14 @@ export async function injectAppiumSymlinks(
|
||||
// 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),
|
||||
),
|
||||
installPaths.map((installPath) => injectAppiumSymlink(path.join(installPath, 'node_modules'), logger)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Omits `driverName`/`pluginName` props from the receipt to make a {@linkcode ExtManifest}
|
||||
*/
|
||||
function receiptToManifest<ExtType extends ExtensionType>(
|
||||
receipt: ExtInstallReceipt<ExtType>,
|
||||
): ExtManifest<ExtType> {
|
||||
function receiptToManifest<ExtType extends ExtensionType>(receipt: ExtInstallReceipt<ExtType>): ExtManifest<ExtType> {
|
||||
if ('driverName' in receipt) {
|
||||
const {driverName, ...manifest} = receipt as ExtInstallReceipt<'driver'>;
|
||||
void driverName;
|
||||
@@ -1299,10 +1204,7 @@ function receiptToManifest<ExtType extends ExtensionType>(
|
||||
* @param pkgName Extension name
|
||||
* @param [pkgVer] Extension version (if not provided then the latest is assumed)
|
||||
*/
|
||||
async function getRemoteExtensionVersionReq(
|
||||
pkgName: string,
|
||||
pkgVer?: string,
|
||||
): Promise<[string, string | null]> {
|
||||
async function getRemoteExtensionVersionReq(pkgName: string, pkgVer?: string): Promise<[string, string | null]> {
|
||||
const allDeps = await npm.getPackageInfo(`${pkgName}${pkgVer ? `@${pkgVer}` : ``}`, [
|
||||
'peerDependencies',
|
||||
'dependencies',
|
||||
@@ -1310,10 +1212,7 @@ async function getRemoteExtensionVersionReq(
|
||||
const requiredVersionPair = Object.values(allDeps)
|
||||
.flatMap((dep) => Object.entries(dep ?? {}))
|
||||
.find(([name]) => name === 'appium');
|
||||
return [
|
||||
npmPackage.version,
|
||||
requiredVersionPair ? (requiredVersionPair[1] as string | null) : null,
|
||||
];
|
||||
return [npmPackage.version, requiredVersionPair ? (requiredVersionPair[1] as string | null) : null];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1338,4 +1237,3 @@ async function injectAppiumSymlink(dstFolder: string, logger: AppiumLogger): Pro
|
||||
}
|
||||
|
||||
export default ExtensionCliCommand;
|
||||
export {ExtensionCliCommand as ExtensionCommand};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
import type {Class, DriverType, ExtensionType, PluginType} from '@appium/types';
|
||||
import type {Args, CliExtensionCommand, CliExtensionSubcommand} from 'appium/types';
|
||||
import type {ExtensionConfig} from '../extension/extension-config';
|
||||
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import type {ExtensionConfig} from '../extension/extension-config';
|
||||
import {isExtensionCommandArgs} from '../schema/cli-args-guards';
|
||||
import DriverCliCommand from './driver-command';
|
||||
import PluginCliCommand from './plugin-command';
|
||||
@@ -25,10 +26,10 @@ export type ExtCommand<ExtType extends ExtensionType> = ExtType extends DriverTy
|
||||
* When JSON output is enabled, this also prints the serialized command result
|
||||
* unless output was suppressed by the caller.
|
||||
*/
|
||||
export async function runExtensionCommand<
|
||||
Cmd extends CliExtensionCommand,
|
||||
SubCmd extends CliExtensionSubcommand,
|
||||
>(args: Args<Cmd, SubCmd>, config: ExtensionConfig<Cmd>) {
|
||||
export async function runExtensionCommand<Cmd extends CliExtensionCommand, SubCmd extends CliExtensionSubcommand>(
|
||||
args: Args<Cmd, SubCmd>,
|
||||
config: ExtensionConfig<Cmd>,
|
||||
) {
|
||||
// TODO driver config file should be locked while any of these commands are
|
||||
// running to prevent weird situations
|
||||
let jsonResult: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {ArgumentParser} from 'argparse';
|
||||
import type {SubArgumentParserOptions, SubParser} from 'argparse';
|
||||
import path from 'node:path';
|
||||
import {setPath} from '../utils';
|
||||
|
||||
import type {DriverType, PluginType} from '@appium/types';
|
||||
import type {CliExtensionSubcommand} from 'appium/types';
|
||||
import {ArgumentParser} from 'argparse';
|
||||
import type {SubArgumentParserOptions, SubparsersAction} from 'argparse';
|
||||
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_DOCTOR,
|
||||
@@ -16,18 +17,19 @@ import {
|
||||
SERVER_SUBCOMMAND,
|
||||
SETUP_SUBCOMMAND,
|
||||
} from '../constants';
|
||||
import {finalizeSchema, getAllArgSpecs, getArgSpec, hasArgSpec} from '../schema';
|
||||
import {APPIUM_VER} from '../helpers/build';
|
||||
import {finalizeSchema, getAllArgSpecs, getArgSpec, hasArgSpec} from '../schema';
|
||||
import {setPath} from '../utils';
|
||||
import {getExtensionArgs, getServerArgs} from './args';
|
||||
import type {ArgumentDefinitions} from './args';
|
||||
import {
|
||||
DEFAULT_PLUGINS,
|
||||
SUBCOMMAND_MOBILE,
|
||||
SUBCOMMAND_DESKTOP,
|
||||
SUBCOMMAND_BROWSER,
|
||||
SUBCOMMAND_RESET,
|
||||
getPresetDrivers,
|
||||
determinePlatformName,
|
||||
getPresetDrivers,
|
||||
SUBCOMMAND_BROWSER,
|
||||
SUBCOMMAND_DESKTOP,
|
||||
SUBCOMMAND_MOBILE,
|
||||
SUBCOMMAND_RESET,
|
||||
} from './setup-command';
|
||||
|
||||
export const EXTRA_ARGS = 'extraArgs';
|
||||
@@ -37,16 +39,7 @@ export const EXTRA_ARGS = 'extraArgs';
|
||||
* will automatically inject the `server` subcommand.
|
||||
*/
|
||||
const NON_SERVER_ARGS = Object.freeze(
|
||||
new Set([
|
||||
SETUP_SUBCOMMAND,
|
||||
DRIVER_TYPE,
|
||||
PLUGIN_TYPE,
|
||||
SERVER_SUBCOMMAND,
|
||||
'-h',
|
||||
'--help',
|
||||
'-v',
|
||||
'--version',
|
||||
]),
|
||||
new Set([SETUP_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE, SERVER_SUBCOMMAND, '-h', '--help', '-v', '--version']),
|
||||
);
|
||||
|
||||
type LooseArgsMap = {[key: string]: any};
|
||||
@@ -134,10 +127,7 @@ export class ArgParser {
|
||||
*
|
||||
* E.g., `{'driver-foo-bar': baz}` becomes `{driver: {foo: {bar: 'baz'}}}`
|
||||
*/
|
||||
private static _transformParsedArgs(
|
||||
args: LooseArgsMap,
|
||||
unknownArgs: string[] = [],
|
||||
): TransformedArgsMap {
|
||||
private static _transformParsedArgs(args: LooseArgsMap, unknownArgs: string[] = []): TransformedArgsMap {
|
||||
const result = Object.entries(args).reduce((unpacked: LooseArgsMap, [key, value]) => {
|
||||
const spec = hasArgSpec(key) ? getArgSpec(key) : undefined;
|
||||
if (value !== undefined && spec) {
|
||||
@@ -168,7 +158,7 @@ export class ArgParser {
|
||||
/**
|
||||
* Adds the `server` subcommand parser and returns its argument definitions.
|
||||
*/
|
||||
private static _addServerToParser(subParser: SubParser): ArgumentDefinitions {
|
||||
private static _addServerToParser(subParser: SubparsersAction): ArgumentDefinitions {
|
||||
const serverParser = subParser.add_parser('server', {
|
||||
add_help: true,
|
||||
help: 'Start an Appium server',
|
||||
@@ -189,7 +179,7 @@ export class ArgParser {
|
||||
/**
|
||||
* Adds extension sub-sub-commands to `driver`/`plugin` subcommands
|
||||
*/
|
||||
private static _addExtensionCommandsToParser(subParser: SubParser): void {
|
||||
private static _addExtensionCommandsToParser(subParser: SubparsersAction): void {
|
||||
for (const type of [DRIVER_TYPE, PLUGIN_TYPE] as [DriverType, PluginType]) {
|
||||
const extParser = subParser.add_parser(type, {
|
||||
add_help: true,
|
||||
@@ -262,7 +252,7 @@ export class ArgParser {
|
||||
/**
|
||||
* Add subcommand and sub-sub commands for 'setup' subcommand.
|
||||
*/
|
||||
private static _addSetupToParser(subParser: SubParser): void {
|
||||
private static _addSetupToParser(subParser: SubparsersAction): void {
|
||||
const setupParser = subParser.add_parser('setup', {
|
||||
add_help: true,
|
||||
help: 'Batch install or uninstall Appium drivers and plugins',
|
||||
@@ -330,12 +320,16 @@ export class ArgParser {
|
||||
} else if (knownArgs?.pluginCommand === 'ls') {
|
||||
knownArgs.pluginCommand = 'list';
|
||||
}
|
||||
if (
|
||||
unknownArgs?.length &&
|
||||
(knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')
|
||||
) {
|
||||
if (unknownArgs?.length && (knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')) {
|
||||
return ArgParser._transformParsedArgs(knownArgs, unknownArgs);
|
||||
} else if (unknownArgs?.length) {
|
||||
if (knownArgs.allowUnknownArgs) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`[WARN] Ignoring unrecognized arguments because '--allow-unknown-args' is set: ${unknownArgs.join(' ')}`,
|
||||
);
|
||||
return ArgParser._transformParsedArgs(knownArgs);
|
||||
}
|
||||
throw new Error(`[ERROR] Unrecognized arguments: ${unknownArgs.join(' ')}`);
|
||||
}
|
||||
return ArgParser._transformParsedArgs(knownArgs);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {console, util} from '@appium/support';
|
||||
import type {ExtMetadata, ExtRecord, InstallType} from 'appium/types';
|
||||
import ExtensionCliCommand from './extension-command';
|
||||
|
||||
import {KNOWN_PLUGINS} from '../constants';
|
||||
import {ExtensionCliCommand} from './extension-command';
|
||||
import type {
|
||||
ExtensionArgs,
|
||||
ExtensionCommandOptions,
|
||||
@@ -8,7 +10,6 @@ import type {
|
||||
PostInstallText,
|
||||
RunOutput,
|
||||
} from './extension-command';
|
||||
import {KNOWN_PLUGINS} from '../constants';
|
||||
|
||||
const REQ_PLUGIN_FIELDS = ['pluginName', 'mainClass'];
|
||||
type PluginInstallOpts = {plugin: string; installType: InstallType; packageName?: string};
|
||||
@@ -28,11 +29,7 @@ export default class PluginCliCommand extends ExtensionCliCommand<'plugin'> {
|
||||
*
|
||||
* @param opts - install options
|
||||
*/
|
||||
async install({
|
||||
plugin,
|
||||
installType,
|
||||
packageName,
|
||||
}: PluginInstallOpts): Promise<ExtRecord<'plugin'>> {
|
||||
async install({plugin, installType, packageName}: PluginInstallOpts): Promise<ExtRecord<'plugin'>> {
|
||||
return await super._install({
|
||||
installSpec: plugin,
|
||||
installType,
|
||||
@@ -93,10 +90,7 @@ export default class PluginCliCommand extends ExtensionCliCommand<'plugin'> {
|
||||
* @returns formatted success text
|
||||
*/
|
||||
override getPostInstallText({extName, extData}: ExtensionArgs<'plugin'>): PostInstallText {
|
||||
return console.styleText(
|
||||
'green',
|
||||
`Plugin ${extName}@${extData.version} successfully installed`,
|
||||
);
|
||||
return console.styleText('green', `Plugin ${extName}@${extData.version} successfully installed`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,13 +102,9 @@ export default class PluginCliCommand extends ExtensionCliCommand<'plugin'> {
|
||||
* @param pluginMetadata - `appium` metadata from extension package
|
||||
* @param installSpec - install spec from CLI
|
||||
*/
|
||||
override validateExtensionFields(
|
||||
pluginMetadata: ExtMetadata<'plugin'>,
|
||||
installSpec: string,
|
||||
): void {
|
||||
override validateExtensionFields(pluginMetadata: ExtMetadata<'plugin'>, installSpec: string): void {
|
||||
const missingFields = REQ_PLUGIN_FIELDS.reduce<string[]>(
|
||||
(acc, field) =>
|
||||
pluginMetadata[field as keyof typeof pluginMetadata] ? acc : [...acc, field],
|
||||
(acc, field) => (pluginMetadata[field as keyof typeof pluginMetadata] ? acc : [...acc, field]),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {fs, system} from '@appium/support';
|
||||
import type {
|
||||
Args,
|
||||
CliCommandSetup,
|
||||
@@ -5,11 +6,11 @@ import type {
|
||||
CliExtensionCommand,
|
||||
CliExtensionSubcommand,
|
||||
} from 'appium/types';
|
||||
|
||||
import {DESKTOP_BROWSERS, DESKTOP_DRIVERS, MOBILE_DRIVERS} from '../constants';
|
||||
import {runExtensionCommand} from './extension';
|
||||
import {system, fs} from '@appium/support';
|
||||
import {log} from '../logger';
|
||||
import type {ExtensionConfig} from '../extension/extension-config';
|
||||
import {log} from '../logger';
|
||||
import {runExtensionCommand} from './extension';
|
||||
|
||||
/**
|
||||
* Subcommands of preset for setup
|
||||
@@ -44,9 +45,7 @@ type CliExtArgs = Args<CliExtensionCommand, CliExtensionSubcommand>;
|
||||
/**
|
||||
* Return a list of drivers available for current host platform.
|
||||
*/
|
||||
export function getPresetDrivers(
|
||||
presetName: Exclude<CliCommandSetupSubcommand, 'reset'>,
|
||||
): string[] {
|
||||
export function getPresetDrivers(presetName: Exclude<CliCommandSetupSubcommand, 'reset'>): string[] {
|
||||
return PRESET_PAIRS[presetName].filter((driver) => {
|
||||
if (DRIVERS_ONLY_MACOS.includes(driver)) {
|
||||
return system.isMac();
|
||||
@@ -105,10 +104,7 @@ export async function runSetupCommand(
|
||||
/**
|
||||
* Resets all installed drivers and extensions
|
||||
*/
|
||||
async function resetAllExtensions(
|
||||
driverConfig: DriverConfig,
|
||||
pluginConfig: PluginConfig,
|
||||
): Promise<void> {
|
||||
async function resetAllExtensions(driverConfig: DriverConfig, pluginConfig: PluginConfig): Promise<void> {
|
||||
const commandConfigs: [CliExtensionCommand, DriverConfig | PluginConfig][] = [
|
||||
['driver', driverConfig],
|
||||
['plugin', pluginConfig],
|
||||
@@ -116,11 +112,7 @@ async function resetAllExtensions(
|
||||
for (const [command, config] of commandConfigs) {
|
||||
for (const extensionName of Object.keys(config.installedExtensions)) {
|
||||
try {
|
||||
await uninstallExtension(
|
||||
extensionName,
|
||||
extensionCommandArgs(command, extensionName, 'uninstall'),
|
||||
config,
|
||||
);
|
||||
await uninstallExtension(extensionName, extensionCommandArgs(command, extensionName, 'uninstall'), config);
|
||||
} catch (e) {
|
||||
log.warn(
|
||||
`${extensionName} ${command} cannot be uninstalled. Will delete the manifest anyway. ` +
|
||||
@@ -136,9 +128,7 @@ async function resetAllExtensions(
|
||||
|
||||
await fs.rimraf(manifestPath);
|
||||
if (await fs.exists(manifestPath)) {
|
||||
throw new Error(
|
||||
`${command} manifest at '${manifestPath}' cannot be deleted. Is it accessible?`,
|
||||
);
|
||||
throw new Error(`${command} manifest at '${manifestPath}' cannot be deleted. Is it accessible?`);
|
||||
} else {
|
||||
log.info(`Successfully deleted ${command} manifest at '${manifestPath}'`);
|
||||
}
|
||||
@@ -174,11 +164,7 @@ async function installDrivers(
|
||||
driverConfig: DriverConfig,
|
||||
): Promise<void> {
|
||||
for (const driverName of getPresetDrivers(subcommand)) {
|
||||
await installExtension(
|
||||
driverName,
|
||||
extensionCommandArgs('driver', driverName, 'install'),
|
||||
driverConfig,
|
||||
);
|
||||
await installExtension(driverName, extensionCommandArgs('driver', driverName, 'install'), driverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,11 +173,7 @@ async function installDrivers(
|
||||
*/
|
||||
async function setupDefaultPlugins(pluginConfig: PluginConfig): Promise<void> {
|
||||
for (const pluginName of DEFAULT_PLUGINS) {
|
||||
await installExtension(
|
||||
pluginName,
|
||||
extensionCommandArgs('plugin', pluginName, 'install'),
|
||||
pluginConfig,
|
||||
);
|
||||
await installExtension(pluginName, extensionCommandArgs('plugin', pluginName, 'install'), pluginConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,11 +78,7 @@ export function log(json: boolean, msg: string): void {
|
||||
* @param fn - function to wrap with spinning
|
||||
* @returns result of `fn`
|
||||
*/
|
||||
export async function spinWith<T>(
|
||||
json: boolean,
|
||||
msg: string,
|
||||
fn: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
export async function spinWith<T>(json: boolean, msg: string, fn: () => T | Promise<T>): Promise<T> {
|
||||
if (json) {
|
||||
return await fn();
|
||||
}
|
||||
|
||||
@@ -123,10 +123,7 @@ export class Doctor {
|
||||
manualIssues,
|
||||
);
|
||||
await handleIssues(
|
||||
[
|
||||
'### Optional Manual Fixes ###',
|
||||
'To fix these optional issues, please do the following manually:',
|
||||
],
|
||||
['### Optional Manual Fixes ###', 'To fix these optional issues, please do the following manually:'],
|
||||
manualIssuesOptional,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {util} from '@appium/support';
|
||||
import type {DriverClass, DriverType, StringRecord} from '@appium/types';
|
||||
import type {ExtManifest, ExtName, ExtRecord} from 'appium/types';
|
||||
|
||||
import {DRIVER_TYPE} from '../constants';
|
||||
import {log} from '../logger';
|
||||
import type {ExtManifestProblem} from './extension-config';
|
||||
@@ -48,10 +49,7 @@ export class DriverConfig extends ExtensionConfig<DriverType> {
|
||||
return `${String(driverName)}@${version} (automationName '${automationName}')`;
|
||||
}
|
||||
|
||||
async findMatchingDriver<C extends StringRecord>({
|
||||
automationName,
|
||||
platformName,
|
||||
}: C): Promise<MatchedDriver> {
|
||||
async findMatchingDriver<C extends StringRecord>({automationName, platformName}: C): Promise<MatchedDriver> {
|
||||
if (typeof platformName !== 'string') {
|
||||
throw new Error('You must include a platformName capability');
|
||||
}
|
||||
@@ -66,10 +64,7 @@ export class DriverConfig extends ExtensionConfig<DriverType> {
|
||||
);
|
||||
|
||||
try {
|
||||
const {driverName, mainClass, version} = this._getDriverBySupport(
|
||||
automationName,
|
||||
platformName,
|
||||
);
|
||||
const {driverName, mainClass, version} = this._getDriverBySupport(automationName, platformName);
|
||||
log.info(`The '${driverName}' driver was installed and matched caps.`);
|
||||
log.info(`Will require it at ${this.getInstallPath(driverName)}`);
|
||||
const driver = await this.requireAsync(driverName as ExtName<DriverType>);
|
||||
@@ -90,10 +85,7 @@ export class DriverConfig extends ExtensionConfig<DriverType> {
|
||||
}
|
||||
}
|
||||
|
||||
protected override getConfigProblems(
|
||||
extManifest: ExtManifest<DriverType>,
|
||||
extName: string,
|
||||
): ExtManifestProblem[] {
|
||||
protected override getConfigProblems(extManifest: ExtManifest<DriverType>, extName: string): ExtManifestProblem[] {
|
||||
void extName;
|
||||
const problems: ExtManifestProblem[] = [];
|
||||
const {platformNames, automationName} = extManifest;
|
||||
@@ -146,9 +138,7 @@ export class DriverConfig extends ExtensionConfig<DriverType> {
|
||||
for (const [driverName, driverData] of Object.entries(drivers)) {
|
||||
const {automationName, platformNames} = driverData;
|
||||
const aNameMatches = automationName.toLowerCase() === matchAutomationName.toLowerCase();
|
||||
const pNameMatches = platformNames
|
||||
.map((p) => p.toLowerCase())
|
||||
.includes(matchPlatformName.toLowerCase());
|
||||
const pNameMatches = platformNames.map((p) => p.toLowerCase()).includes(matchPlatformName.toLowerCase());
|
||||
|
||||
if (aNameMatches && pNameMatches) {
|
||||
return {driverName, ...driverData};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user