Compare commits

..
Author SHA1 Message Date
Jonathan Lipps 6fec1bfbeb chore: publish
- appium@3.1.1
 - @appium/base-driver@10.1.1
 - @appium/base-plugin@3.0.4
 - @appium/docutils@2.1.2
 - @appium/driver-test-support@1.0.4
 - @appium/eslint-config-appium-ts@2.0.4
 - @appium/fake-plugin@4.0.4
 - @appium/images-plugin@4.0.3
 - @appium/opencv@4.0.3
 - @appium/plugin-test-support@1.0.3
 - @appium/storage-plugin@1.0.3
 - @appium/support@7.0.3
 - @appium/test-support@4.0.3
 - @appium/types@1.1.1
 - @appium/universal-xml-plugin@2.0.2
2025-11-11 16:03:19 -08:00
Mykola Mokhnach fc273da0ef fix(base-driver): Use shouldUpgradeCallback for proper upgrades processing (#21706) 2025-11-11 21:42:43 +01:00
renovate[bot] f9ecc32a20 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.46.4 (#21709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 16:37:46 +00:00
renovate[bot] b64862c705 chore(support): update dependency sharp to v0.34.5 (#21707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 10:09:33 +02:00
renovate[bot] 85f86c562a chore(support): update dependency axios to v1.13.2 (#21704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 12:10:30 +00:00
github-actions[bot]andmykola-mokhnach 81b90b6918 docs(appium): Update documentation translations (#21702)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2025-11-05 19:12:39 +02:00
renovate[bot] 304e4c4a8b chore(deps): update dependency mocha to v11.7.5 (#21703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:07:20 +00:00
Theo King 4591151515 docs(appium): Update Selenium Grid guide to address breaking changes to 4.35 relay behaviour (#21701) 2025-11-04 21:16:07 +02:00
renovate[bot] f31d2cd5d6 chore(types): update dependency type-fest to v5.2.0 (#21700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 14:04:39 +01:00
renovate[bot] 32ace79b7f chore(eslint-config-appium-ts): update eslint-related packages (#21699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 14:04:21 +01:00
renovate[bot] 54bfa8fc99 chore(deps): update dependency @types/node to v24.10.0 (#21697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 19:47:55 +00:00
renovate[bot] 9188336c20 chore(universal-xml-plugin): update dependency fast-xml-parser to v5.3.1 (#21696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 08:55:59 +00:00
renovate[bot] 7e5ac594d7 chore(docutils): update dependency mkdocs-material to v9.6.23 (#21695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 09:50:35 +00:00
renovate[bot] 46e5dd1b62 chore(docutils): update dependency mkdocs-git-revision-date-localized-plugin to v1.5.0 (#21692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 12:32:16 +00:00
Mykola Mokhnach 8fdb16b3e9 ci: Sort Github stats by PR author first (#21694) 2025-11-01 12:24:14 +01:00
renovate[bot] 5650d04672 chore(eslint-config-appium-ts): update eslint-related packages to v9.39.0 (#21693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 09:20:58 +01:00
renovate[bot] 1d70d08d2b chore(storage-plugin): update dependency rimraf to v6.1.0 (#21690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 08:35:46 +01:00
Kazuaki Matsuo ce219fba33 docs: update a few more in payout.md
Added examples for contribution types and payment details.
2025-10-31 00:09:18 -07:00
Kazuaki Matsuo 1796cbe737 docs: update payout steps by following the new Web UI design 2025-10-30 23:54:45 -07:00
Mykola Mokhnach e72804878d ci: Tune PRs sorting logic in the Slack message (#21689) 2025-10-30 17:07:33 +01:00
Mykola Mokhnach ce68d14087 ci: Only run e2e tests for pull requests (#21688) 2025-10-30 09:24:44 +01:00
renovate[bot] 855fe5c31a chore(deps): update definitelytyped (#21676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 09:21:47 +01:00
renovate[bot] 5f3e697ff6 chore(eslint-config-appium-ts): update dependency @eslint/compat to v1.4.1 (#21687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 09:12:24 +01:00
renovate[bot] 1f621cc190 chore(deps): update dependency @types/node to v22.18.13 (#21678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 09:12:10 +01:00
renovate[bot] e790ad7607 chore(support): update dependency axios to v1.13.1 (#21679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 09:11:52 +01:00
Mykola Mokhnach 0d5ba222d1 test: Update integration tests for compatibility with Node24 (#21685) 2025-10-30 08:59:17 +01:00
Edgars Eglītis b12a47349a fix(driver-test-support): fix test failures due to invalid syntax (#21683) 2025-10-29 15:31:59 +01:00
Mykola Mokhnach 3cac6a77e9 ci: Simplify slack table format (#21686) 2025-10-29 14:10:46 +01:00
Mykola Mokhnach fd156243a2 ci: Tune slack table formatting (#21684) 2025-10-29 13:50:24 +01:00
Mykola Mokhnach 579f1029aa ci: Fix sending messages to Slack (#21682) 2025-10-29 13:20:44 +01:00
renovate[bot] f1e747853e chore(workflows): update slackapi/slack-github-action action to v2.1.1 (#21680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 09:38:09 +02:00
renovate[bot] 0888b14afd chore(workflows): update actions/setup-node action to v6 (#21681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 09:35:32 +02:00
Mykola Mokhnach ac539b0d29 ci: Fail on slack errors 2025-10-29 07:34:23 +01:00
Mykola Mokhnach 6553207df6 ci: Automate Github PRs statistics retrieval (#21677) 2025-10-29 07:22:59 +01:00
Mykola Mokhnach 139a983f25 ci: Report Crowdin translation stats for each month automatically to Slack (#21649) 2025-10-28 20:50:55 +01:00
renovate[bot] b1cef7cb2d chore(support): update dependency axios to v1.13.0 (#21674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 18:36:38 +00:00
renovate[bot] 25ca047314 chore(deps): update dependency @types/express to v5.0.5 (#21673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 11:54:48 +00:00
Mykola Mokhnach 5d62688e8b fix(support): Properly handle moving to another volume (#21672) 2025-10-26 14:30:46 +01:00
renovate[bot] 54ba877943 chore(deps): update definitelytyped (#21670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 12:02:04 +00:00
github-actions[bot]andmykola-mokhnach d27413081a docs(appium): Update documentation translations (#21669)
Co-authored-by: mykola-mokhnach <7767781+mykola-mokhnach@users.noreply.github.com>
2025-10-23 14:43:44 +09:00
Edgars Eglītis 00db7d3325 docs: add Jarvis Appium to ecosystem docs (#21668) 2025-10-22 20:20:41 +03:00
renovate[bot] 2e5c05b404 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.46.2 (#21667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 07:04:09 +00:00
renovate[bot] c6a061926a chore(deps): update definitelytyped (#21666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 17:48:18 +00:00
Mykola Mokhnach 8f5aac3722 fix(appium): Inject symlinks for all installed extensions (#21664) 2025-10-20 17:36:30 +02:00
renovate[bot] d12a6a53cf chore(eslint-config-appium-ts): update eslint-related packages to v9.38.0 (#21663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 08:46:55 +00:00
renovate[bot] bc192cd6be chore(support): update dependency teen_process to v3.0.2 (#21661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 13:28:55 +00:00
renovate[bot] 55ed37f185 chore(deps): update dependency @types/node to v22.18.11 (#21660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 09:06:22 +00:00
renovate[bot] d77abf141f chore(docutils): update dependency mkdocs-material to v9.6.22 (#21658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 15:59:53 +00:00
renovate[bot] 791e2679c4 chore(workflows): update actions/setup-node action to v6 (#21655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 16:22:20 +03:00
renovate[bot] 0a68b5be52 chore(eslint-config-appium-ts): update dependency typescript-eslint to v8.46.1 (#21654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 09:10:55 +00:00
renovate[bot] e075875ee3 chore(types): update dependency type-fest to v5.1.0 (#21651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 13:57:16 +00:00
renovate[bot] e6b4018866 chore(deps): update dependency @types/node to v22.18.10 (#21650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 13:33:58 +00:00
renovate[bot] ce9f5cd2e2 chore(deps): update dependency @types/node to v22.18.9 (#21648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 09:38:26 +00:00
63 changed files with 2467 additions and 1149 deletions
+50
View File
@@ -0,0 +1,50 @@
name: Setup Node, Install, Build
description: |
Common setup for Appium CI jobs: checkout, setup Node, install deps, optional build, optional V8 cache clear.
inputs:
node-version:
description: Node.js version to setup
required: true
install-command:
description: npm install command
required: false
default: npm ci --foreground-scripts
run-build:
description: Whether to run npm run build
required: false
default: 'true'
clear-v8-cache:
description: Whether to clear V8 code cache (for Node.js 24+ compatibility)
required: false
default: 'true'
runs:
using: composite
steps:
- name: Use Node.js ${{ inputs.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: npm
- name: Install dependencies
uses: bahmutov/npm-install@v1
with:
useRollingCache: true
install-command: ${{ inputs.install-command }}
- name: Build packages
if: ${{ inputs.run-build == 'true' }}
shell: bash
run: npm run build
- name: Clear V8 code cache
if: ${{ inputs.clear-v8-cache == 'true' }}
shell: bash
run: |
# Clear V8 code cache to avoid bytecode deserialization errors
if [[ -d "$HOME/.node" ]]; then
rm -rf "$HOME/.node"/* 2>/dev/null || true
fi
if [[ -d "$HOME/.cache/node" ]]; then
rm -rf "$HOME/.cache/node"/* 2>/dev/null || true
fi
+127 -24
View File
@@ -3,7 +3,6 @@ on:
push:
branches:
- master
- appium3
# this list of paths could potentially impact the build
paths:
- '**/tsconfig*.json'
@@ -25,7 +24,6 @@ on:
pull_request:
branches:
- master
- appium3
paths:
- '**/tsconfig*.json'
- 'packages/base-driver/static/**/*'
@@ -52,7 +50,7 @@ permissions:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
prepare_matrix:
@@ -64,44 +62,149 @@ jobs:
id: generate-matrix
uses: msimerson/node-lts-versions@v1
test:
prepare_e2e_packages:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.discover-packages.outputs.packages }}
steps:
- uses: actions/checkout@v5
- name: Discover packages with e2e tests
id: discover-packages
run: |
packages=()
for package_dir in packages/*/; do
package_json="${package_dir}package.json"
if [[ -f "$package_json" ]]; then
# Check if package.json has test:e2e script and it's not a no-op
has_e2e=$(node -e "
try {
const pkg = require('./$package_json');
const script = pkg.scripts && pkg.scripts['test:e2e'];
if (script && !script.includes('echo') && !script.includes('No e2e tests')) {
console.log('true');
}
} catch(e) {
// Ignore errors
}
")
if [[ "$has_e2e" == "true" ]]; then
# Use the directory path without packages/ prefix (remove trailing slash)
package_name="${package_dir%/}"
package_name="${package_name#packages/}"
packages+=("$package_name")
fi
fi
done
# Convert array to JSON array for GitHub Actions matrix
if [[ ${#packages[@]} -eq 0 ]]; then
echo "No packages with e2e tests found"
packages_json="[]"
else
# Use jq to create a compact single-line JSON array
packages_json=$(printf '%s\n' "${packages[@]}" | jq -R '.' | jq -s '.' | jq -c '.')
fi
echo "packages=$packages_json" >> $GITHUB_OUTPUT
echo "Found packages with e2e tests:"
echo "$packages_json" | jq -r '.[]' | sed 's/^/ - /'
test-smoke:
needs:
- prepare_matrix
name: Tests
name: Smoke Tests
strategy:
matrix:
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
os:
- ubuntu-latest
# TODO: Windows is not stable and slow to run
# - windows-latest
# TODO: Enable below envs after all tests have been verified green
# - macos-latest
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
- name: Setup, install and build
uses: ./.github/actions/setup-build
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
uses: bahmutov/npm-install@v1
with:
useRollingCache: true
install-command: npm ci --foreground-scripts
- name: Run smoke, unit & E2E tests
run: npm run test:ci
run-build: 'true'
clear-v8-cache: 'true'
- name: Run smoke tests
run: npm run test:smoke
test-unit:
needs:
- prepare_matrix
name: Unit Tests
strategy:
matrix:
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup, install and build
uses: ./.github/actions/setup-build
with:
node-version: ${{ matrix.node-version }}
install-command: npm ci --foreground-scripts
run-build: 'true'
clear-v8-cache: 'true'
- name: Run unit tests
run: npm run test:unit
test-types:
needs:
- prepare_matrix
name: Type Tests
strategy:
matrix:
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup, install and build
uses: ./.github/actions/setup-build
with:
node-version: ${{ matrix.node-version }}
install-command: npm ci --foreground-scripts
run-build: 'true'
clear-v8-cache: 'true'
- name: Run type tests
run: npm run test:types
test-e2e:
needs:
- prepare_matrix
- prepare_e2e_packages
name: E2E Tests - ${{ matrix.package_dir }} (Node ${{ matrix.node-version }})
if: ${{ github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }}
package_dir: ${{ fromJSON(needs.prepare_e2e_packages.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup, install and build
uses: ./.github/actions/setup-build
with:
node-version: ${{ matrix.node-version }}
install-command: npm ci --foreground-scripts
run-build: 'true'
clear-v8-cache: 'true'
- name: Run E2E tests for ${{ matrix.package_dir }}
run: |
set -o pipefail
cd "packages/${{ matrix.package_dir }}"
npm run test:e2e
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Setup and install (no build)
uses: ./.github/actions/setup-build
with:
useRollingCache: true
node-version: lts/*
install-command: npm ci
run-build: 'false'
clear-v8-cache: 'false'
- name: ESLint
run: npm run lint:ci
+44
View File
@@ -0,0 +1,44 @@
# Collects translation statistics from Crowdin for the last calendar month
# and posts them to Slack grouped by project, language, and translator
name: Crowdin Translation Statistics
on:
workflow_dispatch:
schedule:
# Run on the first day of each month at 9:00 AM UTC
- cron: 0 9 1 * *
jobs:
collect-stats:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- project_id_var: CROWDIN_DOCS_PROJECT_ID
project_name: appium-documentation
- project_id_var: CROWDIN_INSPECTOR_PROJECT_ID
project_name: appium-inspector
steps:
- uses: actions/checkout@v5
- name: Use Node.js LTS
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Collect Crowdin Stats for ${{ matrix.project_name }}
id: crowdin-stats
run: node ./scripts/crowdin-stats.mjs > crowdin-stats-${{ matrix.project_name }}.json
env:
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@v2.1.1
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload-file-path: crowdin-stats-${{ matrix.project_name }}.json
@@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use Node.js LTS
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'npm'
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: Use Node.js LTS
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'npm'
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Appium
@@ -0,0 +1,37 @@
# Collects GitHub contribution statistics from the Appium organization for the last calendar month
# and posts it to Slack
name: GitHub Contribution Statistics
on:
workflow_dispatch:
schedule:
# Run on the first day of each month at 09:00 AM UTC
- cron: 0 9 1 * *
jobs:
collect-contrib-stats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Use Node.js LTS
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Collect GitHub Contribution Stats
id: github-stats
run: node ./scripts/github-contrib-stats.mjs > github-contrib-stats.json
env:
# 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@v2.1.1
with:
errors: true
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload-file-path: github-contrib-stats.json
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
git config user.name github-actions
git config user.email github-actions@github.com
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies (Node.js)
+27 -20
View File
@@ -4,25 +4,32 @@ This documentation is a note for the opencollective payout.
<img width="200" alt="Screenshot 2024-09-17 at 11 04 32 PM" src="https://github.com/user-attachments/assets/5d598a46-8aa3-41e0-aadc-ce3f34b10f6b">
2. Select `Invoice`
3. Type proper account name in `Who is being paid for this expense?`
- The form depends on the target user
4. Select `Next`
5. Input expense info and complete the form. Then, the user will receive the expense info.
2. Select `Appium` in `Who is paying`
3. Select `Invite someone` and fill in a user name to pay payouts for
4. Select `Invoice` for `Select the type of expense`
5. Check the `I have read and understood the instructions and conditions` agreement
6. Select `No, generate an invoice for me`
- Keep blank for the `Additional invoice information (optional)`
8. Fill in the description in the `Item Description`, date and amount for `Expense Items`
- for TCs:
- `<year-month> contribution as a TC https://github.com/appium/appium/blob/master/GOVERNANCE.md#the-technical-committee and maintaining Appium project itself (development, review etc)`
- i.e. `2024-07 contribution as a TC https://github.com/appium/appium/blob/master/GOVERNANCE.md#the-technical-committee and maintaining Appium project itself (development, review etc)`
- for individuals
- `PR contribution: <PRs>` for PRs. `Community contribution in Q&A forum at https://discuss.appium.io/` for appium forums
- E.g.
- `PR contribution: https://github.com/appium/dotnet-client/pull/810, https://github.com/appium/dotnet-client/pull/808`
- `PR contribution: https://github.com/appium/java-client/pull/2206, https://github.com/appium/java-client/pull/2203 and a few PRs`
- `Community contribution in Q&A forum at https://discuss.appium.io/`
- Translations
- e.g.
- `Translation contributions for https://crowdin.com/project/appium-documentation`
- For Date
- today
- For Amount
- Put the payment amount in USD
9. Fill in title in `Title` in `Additional details`
- Expense title: `<year-month> contribution payout`
- e.g. `2024-07 contribution payout`
- Description in "Set invoice details"
- for TCs:
- `<year-month> contribution as a TC https://github.com/appium/appium/blob/master/GOVERNANCE.md#the-technical-committee and maintaining Appium project itself (development, review etc)`
- i.e. `2024-07 contribution as a TC https://github.com/appium/appium/blob/master/GOVERNANCE.md#the-technical-committee and maintaining Appium project itself (development, review etc)`
- for individuals
- `PR contribution: <PRs>` for PRs. `Community contribution in Q&A forum at https://discuss.appium.io/` for appium forums
- E.g.
- `PR contribution: https://github.com/appium/dotnet-client/pull/810, https://github.com/appium/dotnet-client/pull/808`
- `PR contribution: https://github.com/appium/java-client/pull/2206, https://github.com/appium/java-client/pull/2203 and a few PRs`
- `Community contribution in Q&A forum at https://discuss.appium.io/`
- Amount
- Type the amount in `USD`
6. Approve each expense after getting response for each expense
<img width="500" alt="Screenshot 2024-09-11 at 8 40 35 PM" src="https://github.com/user-attachments/assets/462cda69-1ce7-458c-b319-dda87cf10d65">
10. Submit expense
11. Update the expense url in our payout sheet
12. Approve each expense after getting a response for each expense
+753 -678
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -85,16 +85,16 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/js": "9.37.0",
"@eslint/js": "9.39.1",
"@tsconfig/node20": "20.1.6",
"@types/argparse": "2.0.17",
"@types/archiver": "6.0.3",
"@types/archiver": "7.0.0",
"@types/async-lock": "1.4.2",
"@types/base64-stream": "1.0.5",
"@types/bluebird": "3.5.42",
"@types/chai": "5.2.2",
"@types/chai": "5.2.3",
"@types/chai-as-promised": "8.0.2",
"@types/express": "5.0.3",
"@types/express": "5.0.5",
"@types/jsftp": "2.1.5",
"@types/json-schema": "7.0.15",
"@types/klaw": "3.0.7",
@@ -104,7 +104,7 @@
"@types/mocha": "10.0.10",
"@types/mv": "2.1.4",
"@types/ncp": "2.0.8",
"@types/node": "22.18.8",
"@types/node": "24.10.0",
"@types/pluralize": "0.0.33",
"@types/semver": "7.7.1",
"@types/serve-favicon": "2.5.7",
@@ -118,14 +118,14 @@
"@types/wrap-ansi": "3.0.0",
"@types/ws": "8.18.1",
"@types/xmldom": "0.1.34",
"@types/yargs": "17.0.33",
"@types/yargs": "17.0.34",
"asyncbox": "3.0.0",
"chai": "6.2.0",
"chai-as-promised": "8.0.2",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "6.0.0",
"cross-env": "10.1.0",
"eslint": "9.37.0",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
@@ -139,19 +139,19 @@
"midnight-smoker": "8.0.0",
"mjpeg-consumer": "2.0.0",
"mjpeg-server": "0.3.1",
"mocha": "11.7.4",
"mocha": "11.7.5",
"npm-run-all2": "8.0.4",
"prettier": "3.6.2",
"rewiremock": "3.14.6",
"rimraf": "6.0.1",
"rimraf": "6.1.0",
"serve-static": "2.2.0",
"sinon": "21.0.0",
"sync-monorepo-packages": "1.0.2",
"teen_process": "3.0.1",
"teen_process": "3.0.2",
"ts-node": "10.9.2",
"tsd": "0.33.0",
"typescript": "5.9.3",
"typescript-eslint": "8.46.0",
"typescript-eslint": "8.46.4",
"webdriverio": "9.20.0",
"ws": "8.18.3",
"yaml-js": "0.3.1"
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.1.1](https://github.com/appium/appium/compare/appium@3.1.0...appium@3.1.1) (2025-11-12)
### Bug Fixes
* **appium:** Inject symlinks for all installed extensions ([#21664](https://github.com/appium/appium/issues/21664)) ([8f5aac3](https://github.com/appium/appium/commit/8f5aac3722c9572d61f7e6bef20fe43bb14e0674))
## [3.1.0](https://github.com/appium/appium/compare/appium@3.0.2...appium@3.1.0) (2025-10-08)
@@ -55,3 +55,29 @@ Supported by: `@AppiumTestDistribution`
```sh title="Install This Tool"
npm install -g appium-installer
```
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
automation capabilities using Appium.
Supported by: `@AppiumTestDistribution`
```json title="Install This Tool"
// Add the following to your MCP client configuration
{
"mcpServers": {
"jarvis-appium": {
"disabled": false,
"timeout": 100,
"type": "stdio",
"command": "npx",
"args": ["jarvis-appium"],
"env": {
"ANDROID_HOME": "/path/to/android/sdk",
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
}
}
}
}
```
+20 -8
View File
@@ -22,9 +22,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8100
mjpegServerPort: 9100
mjpegScreenshotUrl: "http://localhost:9100"
appium:wdaLocalPort: 8100
appium:mjpegServerPort: 9100
appium:mjpegScreenshotUrl: "http://localhost:9100"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 17"
```
In the above YAML config file, we specify the Appium server port, the driver used, and parameters
@@ -39,9 +41,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8110
mjpegServerPort: 9110
mjpegScreenshotUrl: "http://localhost:9110"
appium:wdaLocalPort: 8110
appium:mjpegServerPort: 9110
appium:mjpegScreenshotUrl: "http://localhost:9110"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 16"
```
### Define the Grid node configs
@@ -60,11 +64,15 @@ port = 5555
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4723"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 13\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
@@ -76,11 +84,15 @@ port = 5565
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4733"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 12\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
@@ -59,3 +59,29 @@ Supported by: `@AppiumTestDistribution`
```sh title="Install This Tool"
npm install -g appium-installer
```
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
automation capabilities using Appium.
Supported by: `@AppiumTestDistribution`
```json title="Install This Tool"
// Add the following to your MCP client configuration
{
"mcpServers": {
"jarvis-appium": {
"disabled": false,
"timeout": 100,
"type": "stdio",
"command": "npx",
"args": ["jarvis-appium"],
"env": {
"ANDROID_HOME": "/path/to/android/sdk",
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
}
}
}
}
```
+20 -8
View File
@@ -22,9 +22,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8100
mjpegServerPort: 9100
mjpegScreenshotUrl: "http://localhost:9100"
appium:wdaLocalPort: 8100
appium:mjpegServerPort: 9100
appium:mjpegScreenshotUrl: "http://localhost:9100"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 17"
```
In the above YAML config file, we specify the Appium server port, the driver used, and parameters
@@ -39,9 +41,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8110
mjpegServerPort: 9110
mjpegScreenshotUrl: "http://localhost:9110"
appium:wdaLocalPort: 8110
appium:mjpegServerPort: 9110
appium:mjpegScreenshotUrl: "http://localhost:9110"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 16"
```
### Define the Grid node configs
@@ -60,11 +64,15 @@ port = 5555
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4723"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 13\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
@@ -76,11 +84,15 @@ port = 5565
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4733"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 12\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
@@ -59,3 +59,29 @@ Supported by: `@AppiumTestDistribution`
```sh title="Install This Tool"
npm install -g appium-installer
```
### [Jarvis Appium](https://github.com/AppiumTestDistribution/mcp-appium)
Jarvis Appium is a Model Context Protocol (MCP) server that provides mobile test development and
automation capabilities using Appium.
Supported by: `@AppiumTestDistribution`
```json title="Install This Tool"
// Add the following to your MCP client configuration
{
"mcpServers": {
"jarvis-appium": {
"disabled": false,
"timeout": 100,
"type": "stdio",
"command": "npx",
"args": ["jarvis-appium"],
"env": {
"ANDROID_HOME": "/path/to/android/sdk",
"CAPABILITIES_CONFIG": "/path/to/your/capabilities.json"
}
}
}
}
```
+20 -8
View File
@@ -22,9 +22,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8100
mjpegServerPort: 9100
mjpegScreenshotUrl: "http://localhost:9100"
appium:wdaLocalPort: 8100
appium:mjpegServerPort: 9100
appium:mjpegScreenshotUrl: "http://localhost:9100"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 17"
```
In the above YAML config file, we specify the Appium server port, the driver used, and parameters
@@ -39,9 +41,11 @@ server:
use-drivers:
- xcuitest
default-capabilities:
wdaLocalPort: 8110
mjpegServerPort: 9110
mjpegScreenshotUrl: "http://localhost:9110"
appium:wdaLocalPort: 8110
appium:mjpegServerPort: 9110
appium:mjpegScreenshotUrl: "http://localhost:9110"
appium:platformVersion: "26.0"
appium:deviceName: "iPhone 16"
```
### Define the Grid node configs
@@ -60,11 +64,15 @@ port = 5555
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4723"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 13\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
@@ -76,11 +84,15 @@ port = 5565
[node]
detect-drivers = false
[events]
publish = "tcp://HUB_IP_ADDRESS:4442"
subscribe = "tcp://HUB_IP_ADDRESS:4443"
[relay]
url = "http://localhost:4733"
status-endpoint = "/status"
configs = [
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 12\", \"appium:automationName\": \"XCUITest\"}"
"1", "{\"platformName\": \"iOS\", \"appium:automationName\": \"XCUITest\"}"
]
```
+10 -1
View File
@@ -454,9 +454,17 @@ class ExtensionCliCommand {
return {pkg, installPath};
});
/** @type {Promise<void>[]} */
const symlinkInjectionPromises = _.uniq([
...Object.values(this.config.installedExtensions).map((ext) => ext.installPath),
installPath,
]).map((instPath) => injectAppiumSymlink.bind(this)(path.join(instPath, 'node_modules')));
// After the extension is installed, we try to inject the appium module symlink
// into the extension's node_modules folder if it is not there yet.
await injectAppiumSymlink.bind(this)(path.join(installPath, 'node_modules'));
// We also inject the symlink into other installed extensions' node_modules folders
// as these might be cleaned up unexpectedly by npm
// (see https://github.com/appium/python-client/pull/1177#issuecomment-3419826643).
await Promise.all(symlinkInjectionPromises);
return this.getInstallationReceipt({
pkg,
@@ -736,6 +744,7 @@ class ExtensionCliCommand {
pkgName,
pkgVer: version,
});
delete extData[/** @type {string} */ (`${this.type}Name`)];
await this.config.updateExtension(installSpec, extData);
}
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "3.1.0",
"version": "3.1.1",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,20 +60,20 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^10.1.0",
"@appium/base-plugin": "^3.0.3",
"@appium/docutils": "^2.1.1",
"@appium/base-driver": "^10.1.1",
"@appium/base-plugin": "^3.0.4",
"@appium/docutils": "^2.1.2",
"@appium/logger": "^2.0.2",
"@appium/schema": "^1.0.0",
"@appium/support": "^7.0.2",
"@appium/types": "^1.1.0",
"@appium/support": "^7.0.3",
"@appium/types": "^1.1.1",
"@sidvind/better-ajv-errors": "4.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"argparse": "2.0.1",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.12.2",
"axios": "1.13.2",
"bluebird": "3.7.2",
"lilconfig": "3.1.3",
"lodash": "4.17.21",
@@ -83,8 +83,8 @@
"resolve-from": "5.0.0",
"semver": "7.7.3",
"source-map-support": "0.5.21",
"teen_process": "3.0.1",
"type-fest": "5.0.1",
"teen_process": "3.0.2",
"type-fest": "5.2.0",
"winston": "3.18.3",
"wrap-ansi": "7.0.0",
"ws": "8.18.3",
@@ -720,6 +720,12 @@ describe('Bidi over SSL', function () {
chai.use(chaiAsPromised.default);
should = chai.should();
// Skip on Node.js 24+ due to spdy package incompatibility (http_parser removed)
const nodeMajorVersion = parseInt(process.version.slice(1).split('.')[0], 10);
if (nodeMajorVersion >= 24) {
return this.skip();
}
try {
await generateCertificate(certPath, keyPath);
} catch (e) {
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [10.1.1](https://github.com/appium/appium/compare/@appium/base-driver@10.1.0...@appium/base-driver@10.1.1) (2025-11-12)
### Bug Fixes
* **base-driver:** Use shouldUpgradeCallback for proper upgrades processing ([#21706](https://github.com/appium/appium/issues/21706)) ([fc273da](https://github.com/appium/appium/commit/fc273da0ef04d3362f10c710f7c8692d7a03fd21))
## [10.1.0](https://github.com/appium/appium/compare/@appium/base-driver@10.0.1...@appium/base-driver@10.1.0) (2025-10-08)
+35 -16
View File
@@ -86,6 +86,39 @@ export function defaultToJSONContentType(req, res, next) {
next();
}
/**
* Core function to handle WebSocket upgrade requests by matching the request path
* against registered WebSocket handlers in the webSocketsMapping.
*
* @param {import('http').IncomingMessage} req - The HTTP request
* @param {import('stream').Duplex} socket - The network socket
* @param {Buffer} head - The first packet of the upgraded stream
* @param {import('@appium/types').StringRecord<import('@appium/types').WSServer>} webSocketsMapping - Mapping of paths to WebSocket servers
* @returns {boolean} - Returns true if the upgrade was handled, false otherwise
*/
export function tryHandleWebSocketUpgrade(req, socket, head, webSocketsMapping) {
if (_.toLower(req.headers?.upgrade) !== 'websocket') {
return false;
}
let currentPathname;
try {
currentPathname = new URL(req.url ?? '', 'http://localhost').pathname;
} catch {
currentPathname = req.url ?? '';
}
for (const [pathname, wsServer] of _.toPairs(webSocketsMapping)) {
if (match(pathname)(currentPathname)) {
wsServer.handleUpgrade(req, socket, head, (ws) => {
wsServer.emit('connection', ws, req);
});
return true;
}
}
log.info(`Did not match the websocket upgrade request at ${currentPathname} to any known route`);
return false;
}
/**
*
* @param {import('@appium/types').StringRecord<import('@appium/types').WSServer>} webSocketsMapping
@@ -93,23 +126,9 @@ export function defaultToJSONContentType(req, res, next) {
*/
export function handleUpgrade(webSocketsMapping) {
return (req, res, next) => {
if (!req.headers?.upgrade || _.toLower(req.headers.upgrade) !== 'websocket') {
return next();
if (tryHandleWebSocketUpgrade(req, req.socket, Buffer.from(''), webSocketsMapping)) {
return;
}
let currentPathname;
try {
currentPathname = new URL(req.url ?? '').pathname;
} catch {
currentPathname = req.url ?? '';
}
for (const [pathname, wsServer] of _.toPairs(webSocketsMapping)) {
if (match(pathname)(currentPathname)) {
return wsServer.handleUpgrade(req, req.socket, Buffer.from(''), (ws) => {
wsServer.emit('connection', ws, req);
});
}
}
log.info(`Did not match the websocket upgrade request at ${currentPathname} to any known route`);
next();
};
}
+182 -140
View File
@@ -14,6 +14,7 @@ import {
allowCrossDomainAsyncExecute,
handleIdempotency,
handleUpgrade,
tryHandleWebSocketUpgrade,
catch404Handler,
handleLogContext,
} from './middleware';
@@ -31,6 +32,158 @@ import {fs, timing} from '@appium/support';
const KEEP_ALIVE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
/**
*
* @param {ServerOpts} opts
* @returns {Promise<AppiumServer>}
*/
export async function server(opts) {
const {
routeConfiguringFunction,
port,
hostname,
cliArgs = /** @type {import('@appium/types').ServerArgs} */ ({}),
allowCors = true,
basePath = DEFAULT_BASE_PATH,
extraMethodMap = {},
serverUpdaters = [],
keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS,
requestTimeout,
} = opts;
const app = express();
const httpServer = await createServer(app, cliArgs);
return await new B(async (resolve, reject) => {
// we put an async function as the promise constructor because we want some things to happen in
// serial (application of plugin updates, for example). But we still need to use a promise here
// because some elements of server start failure only happen in httpServer listeners. So the
// way we resolve it is to use an async function here but to wrap all the inner logic in
// try/catch so any errors can be passed to reject.
try {
const appiumServer = configureHttp({
httpServer,
reject,
keepAliveTimeout,
gracefulShutdownTimeout: cliArgs.shutdownTimeout,
});
const useLegacyUpgradeHandler = !hasShouldUpgradeCallback(httpServer);
configureServer({
app,
addRoutes: routeConfiguringFunction,
allowCors,
basePath,
extraMethodMap,
webSocketsMapping: appiumServer.webSocketsMapping,
useLegacyUpgradeHandler,
});
// allow extensions to update the app and http server objects
for (const updater of serverUpdaters) {
await updater(app, appiumServer, cliArgs);
}
// once all configurations and updaters have been applied, make sure to set up a catchall
// handler so that anything unknown 404s. But do this after everything else since we don't
// want to block extensions' ability to add routes if they want.
app.all('/*all', catch404Handler);
await startServer({
httpServer,
hostname,
port,
keepAliveTimeout,
requestTimeout,
});
resolve(appiumServer);
} catch (err) {
reject(err);
}
});
}
/**
* Sets up some Express middleware and stuff
* @param {ConfigureServerOpts} opts
*/
export function configureServer({
app,
addRoutes,
allowCors = true,
basePath = DEFAULT_BASE_PATH,
extraMethodMap = {},
webSocketsMapping = {},
useLegacyUpgradeHandler = true,
}) {
basePath = normalizeBasePath(basePath);
app.use(endLogFormatter);
app.use(handleLogContext);
// set up static assets
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
// eslint-disable-next-line import/no-named-as-default-member
app.use(express.static(STATIC_DIR));
// crash routes, for testing
app.use(`${basePath}/produce_error`, produceError);
app.use(`${basePath}/crash`, produceCrash);
// Only use legacy Express middleware for WebSocket upgrades if shouldUpgradeCallback is not available
// When shouldUpgradeCallback is available, upgrades are handled directly on the HTTP server
// to avoid Express middleware timeout issues with long-lived connections
if (useLegacyUpgradeHandler) {
app.use(handleUpgrade(webSocketsMapping));
}
if (allowCors) {
app.use(allowCrossDomain);
} else {
app.use(allowCrossDomainAsyncExecute(basePath));
}
app.use(handleIdempotency);
app.use(defaultToJSONContentType);
app.use(bodyParser.urlencoded({extended: true}));
app.use(methodOverride());
app.use(catchAllHandler);
// make sure appium never fails because of a file size upload limit
app.use(bodyParser.json({limit: '1gb'}));
// set up start logging (which depends on bodyParser doing its thing)
app.use(startLogFormatter);
addRoutes(app, {basePath, extraMethodMap});
// dynamic routes for testing, etc.
app.all('/welcome', welcome);
app.all('/test/guinea-pig', guineaPig);
app.all('/test/guinea-pig-scrollable', guineaPigScrollable);
app.all('/test/guinea-pig-app-banner', guineaPigAppBanner);
}
/**
* Normalize base path string
* @param {string} basePath
* @returns {string}
*/
export function normalizeBasePath(basePath) {
if (!_.isString(basePath)) {
throw new Error(`Invalid path prefix ${basePath}`);
}
// ensure the path prefix does not end in '/', since our method map
// starts all paths with '/'
basePath = basePath.replace(/\/$/, '');
// likewise, ensure the path prefix does always START with /, unless the path
// is empty meaning no base path at all
if (basePath !== '' && !basePath.startsWith('/')) {
basePath = `/${basePath}`;
}
return basePath;
}
/**
*
* @param {import('express').Express} app
@@ -70,127 +223,6 @@ async function createServer (app, cliArgs) {
}, app);
}
/**
*
* @param {ServerOpts} opts
* @returns {Promise<AppiumServer>}
*/
export async function server(opts) {
const {
routeConfiguringFunction,
port,
hostname,
cliArgs = /** @type {import('@appium/types').ServerArgs} */ ({}),
allowCors = true,
basePath = DEFAULT_BASE_PATH,
extraMethodMap = {},
serverUpdaters = [],
keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS,
requestTimeout,
} = opts;
const app = express();
const httpServer = await createServer(app, cliArgs);
return await new B(async (resolve, reject) => {
// we put an async function as the promise constructor because we want some things to happen in
// serial (application of plugin updates, for example). But we still need to use a promise here
// because some elements of server start failure only happen in httpServer listeners. So the
// way we resolve it is to use an async function here but to wrap all the inner logic in
// try/catch so any errors can be passed to reject.
try {
const appiumServer = configureHttp({
httpServer,
reject,
keepAliveTimeout,
gracefulShutdownTimeout: cliArgs.shutdownTimeout,
});
configureServer({
app,
addRoutes: routeConfiguringFunction,
allowCors,
basePath,
extraMethodMap,
webSocketsMapping: appiumServer.webSocketsMapping,
});
// allow extensions to update the app and http server objects
for (const updater of serverUpdaters) {
await updater(app, appiumServer, cliArgs);
}
// once all configurations and updaters have been applied, make sure to set up a catchall
// handler so that anything unknown 404s. But do this after everything else since we don't
// want to block extensions' ability to add routes if they want.
app.all('/*all', catch404Handler);
await startServer({
httpServer,
hostname,
port,
keepAliveTimeout,
requestTimeout,
});
resolve(appiumServer);
} catch (err) {
reject(err);
}
});
}
/**
* Sets up some Express middleware and stuff
* @param {ConfigureServerOpts} opts
*/
export function configureServer({
app,
addRoutes,
allowCors = true,
basePath = DEFAULT_BASE_PATH,
extraMethodMap = {},
webSocketsMapping = {},
}) {
basePath = normalizeBasePath(basePath);
app.use(endLogFormatter);
app.use(handleLogContext);
// set up static assets
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
// eslint-disable-next-line import/no-named-as-default-member
app.use(express.static(STATIC_DIR));
// crash routes, for testing
app.use(`${basePath}/produce_error`, produceError);
app.use(`${basePath}/crash`, produceCrash);
app.use(handleUpgrade(webSocketsMapping));
if (allowCors) {
app.use(allowCrossDomain);
} else {
app.use(allowCrossDomainAsyncExecute(basePath));
}
app.use(handleIdempotency);
app.use(defaultToJSONContentType);
app.use(bodyParser.urlencoded({extended: true}));
app.use(methodOverride());
app.use(catchAllHandler);
// make sure appium never fails because of a file size upload limit
app.use(bodyParser.json({limit: '1gb'}));
// set up start logging (which depends on bodyParser doing its thing)
app.use(startLogFormatter);
addRoutes(app, {basePath, extraMethodMap});
// dynamic routes for testing, etc.
app.all('/welcome', welcome);
app.all('/test/guinea-pig', guineaPig);
app.all('/test/guinea-pig-scrollable', guineaPigScrollable);
app.all('/test/guinea-pig-app-banner', guineaPigAppBanner);
}
/**
* Monkeypatches the `http.Server` instance and returns a {@linkcode AppiumServer}.
* This function _mutates_ the `httpServer` parameter.
@@ -212,6 +244,20 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
return Boolean(this['_spdyState']?.secure);
};
// This avoids Express middleware timeout issues with long-lived WebSocket connections
// See: https://github.com/appium/appium/issues/20760
// See: https://github.com/nodejs/node/pull/59824
if (hasShouldUpgradeCallback(httpServer)) {
// shouldUpgradeCallback only returns a boolean to indicate if the upgrade should proceed
// eslint-disable-next-line dot-notation
appiumServer['shouldUpgradeCallback'] = (req) => _.toLower(req.headers?.upgrade) === 'websocket';
appiumServer.on('upgrade', (req, socket, head) => {
if (!tryHandleWebSocketUpgrade(req, socket, head, appiumServer.webSocketsMapping)) {
socket.destroy();
}
});
}
// http.Server.close() only stops new connections, but we need to wait until
// all connections are closed and the `close` event is emitted
const originalClose = appiumServer.close.bind(appiumServer);
@@ -294,29 +340,23 @@ async function startServer({
}
/**
* Normalize base path string
* @param {string} basePath
* @returns {string}
* Checks if the provided server instance supports `shouldUpgradeCallback`.
* This feature was added in Node.js v22.21.0 (LTS) and v24.9.0.
* @param {import('http').Server} server - The HTTP server instance to check
* @returns {boolean}
*/
export function normalizeBasePath(basePath) {
if (!_.isString(basePath)) {
throw new Error(`Invalid path prefix ${basePath}`);
function hasShouldUpgradeCallback(server) {
// Check if shouldUpgradeCallback is available on http.Server
// This is a runtime check that works regardless of TypeScript types
try {
// Use bracket notation to access property that may not exist in type definitions
// eslint-disable-next-line dot-notation
return typeof server['shouldUpgradeCallback'] !== 'undefined';
} catch {
return false;
}
// ensure the path prefix does not end in '/', since our method map
// starts all paths with '/'
basePath = basePath.replace(/\/$/, '');
// likewise, ensure the path prefix does always START with /, unless the path
// is empty meaning no base path at all
if (basePath !== '' && !basePath.startsWith('/')) {
basePath = `/${basePath}`;
}
return basePath;
}
/**
* Options for {@linkcode startServer}.
* @typedef StartServerOpts
@@ -386,4 +426,6 @@ export function normalizeBasePath(basePath) {
* @property {string} [basePath]
* @property {MethodMap} [extraMethodMap]
* @property {import('@appium/types').StringRecord} [webSocketsMapping={}]
* @property {boolean} [useLegacyUpgradeHandler=true] - Whether to use legacy Express middleware for WebSocket upgrades.
* Set to false when using shouldUpgradeCallback on the HTTP server (Node.js >= 22.21.0 or >= 24.9.0).
*/
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "10.1.0",
"version": "10.1.1",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -44,12 +44,12 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^7.0.2",
"@appium/types": "^1.1.0",
"@appium/support": "^7.0.3",
"@appium/types": "^1.1.1",
"@colors/colors": "1.6.0",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.12.2",
"axios": "1.13.2",
"bluebird": "3.7.2",
"body-parser": "2.2.0",
"express": "5.1.0",
@@ -62,7 +62,7 @@
"path-to-regexp": "8.3.0",
"serve-favicon": "2.5.1",
"source-map-support": "0.5.21",
"type-fest": "5.0.1"
"type-fest": "5.2.0"
},
"optionalDependencies": {
"spdy": "4.0.2"
@@ -16,12 +16,13 @@ driverE2ETestSuite(BaseDriver, {
describe('BaseDriver', function () {
let port;
let baseUrl;
let expect;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
port = await getTestPort();
baseUrl = `http://${TEST_HOST}:${port}`;
@@ -52,12 +53,7 @@ describe('BaseDriver', function () {
url: `${baseUrl}/session/${sessionId}/appium/capabilities`,
method: 'GET',
});
data.should.eql({
value: {
capabilities: DEFAULT_CAPS
},
sessionId
});
expect(data.value.capabilities).to.eql(DEFAULT_CAPS);
});
});
});
@@ -15,48 +15,35 @@ function getFixture(file) {
}
describe('app download and configuration', function () {
let expect;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
});
describe('configureApp', function () {
it('should get the path for a local .app', async function () {
let newAppPath = await configureApp(getFixture('FakeIOSApp.app'), '.app');
newAppPath.should.contain('FakeIOSApp.app');
expect(newAppPath).to.contain('FakeIOSApp.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
expect(contents).to.eql('this is not really an app\n');
});
it('should get the path for a local .apk', async function () {
let newAppPath = await configureApp(getFixture('FakeAndroidApp.apk'), '.apk');
newAppPath.should.contain('FakeAndroidApp.apk');
expect(newAppPath).to.contain('FakeAndroidApp.apk');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
});
it('should unzip and get the path for a local .app.zip', async function () {
let newAppPath = await configureApp(getFixture('FakeIOSApp.app.zip'), '.app');
newAppPath.should.contain('FakeIOSApp.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
});
it('should unzip and get the path for a local .ipa', async function () {
let newAppPath = await configureApp(getFixture('FakeIOSApp.ipa'), '.app');
newAppPath.should.contain('FakeIOSApp.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
});
it('should fail for a bad zip file', async function () {
await configureApp(getFixture('BadZippedApp.zip'), '.app').should.be.rejectedWith(/PK/);
expect(contents).to.eql('this is not really an apk\n');
});
it('should fail if extensions do not match', async function () {
await configureApp(getFixture('FakeIOSApp.app'), '.wrong').should.be.rejectedWith(
await expect(configureApp(getFixture('FakeIOSApp.app'), '.wrong')).to.be.rejectedWith(
/did not have extension/
);
});
it('should fail if zip file does not contain an app whose extension matches', async function () {
await configureApp(getFixture('FakeIOSApp.app.zip'), '.wrong').should.be.rejectedWith(
await expect(configureApp(getFixture('FakeIOSApp.app.zip'), '.wrong')).to.be.rejectedWith(
/did not have extension/
);
});
@@ -71,10 +58,10 @@ describe('app download and configuration', function () {
describe('server not available', function () {
it('should handle server not available', async function () {
await configureApp(
await expect(configureApp(
`${serverUrl}/FakeIOSApp.app.zip`,
'.app'
).should.eventually.be.rejectedWith(/ECONNREFUSED/);
)).to.eventually.be.rejectedWith(/ECONNREFUSED/);
});
});
describe('server available', function () {
@@ -119,101 +106,61 @@ describe('app download and configuration', function () {
await server.close();
});
it('should download zip file', async function () {
let newAppPath = await configureApp(`${serverUrl}/FakeIOSApp.app.zip`, '.app');
newAppPath.should.contain('FakeIOSApp.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
});
it('should download zip file with query string', async function () {
it('should download apk file with query string', async function () {
let newAppPath = await configureApp(
`${serverUrl}/FakeIOSApp.app.zip?sv=abc&sr=def`,
'.app'
`${serverUrl}/FakeAndroidApp.apk?sv=abc&sr=def`,
'.apk'
);
newAppPath.should.contain('.app');
expect(newAppPath).to.contain('.apk');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
expect(contents).to.eql('this is not really an apk\n');
});
it('should download an app file', async function () {
let newAppPath = await configureApp(`${serverUrl}/FakeIOSApp.app`, '.app');
newAppPath.should.contain('.app');
expect(newAppPath).to.contain('.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
expect(contents).to.eql('this is not really an app\n');
});
it('should accept multiple extensions', async function () {
let newAppPath = await configureApp(`${serverUrl}/FakeIOSApp.app.zip`, ['.app', '.aab']);
newAppPath.should.contain('FakeIOSApp.app');
let newAppPath = await configureApp(`${serverUrl}/FakeIOSApp.app`, ['.app', '.aab']);
expect(newAppPath).to.contain('FakeIOSApp.app');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an app\n');
expect(contents).to.eql('this is not really an app\n');
});
it('should download an apk file', async function () {
let newAppPath = await configureApp(`${serverUrl}/FakeAndroidApp.apk`, '.apk');
newAppPath.should.contain('.apk');
expect(newAppPath).to.contain('.apk');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
expect(contents).to.eql('this is not really an apk\n');
});
it('should handle zip file that cannot be downloaded', async function () {
await configureApp(`${serverUrl}/missing/FakeIOSApp.app.zip`, '.app').should.eventually.be
await expect(configureApp(`${serverUrl}/missing/FakeIOSApp.app.zip`, '.app')).to.eventually.be
.rejected;
});
it('should handle invalid protocol', async function () {
await configureApp(
await expect(configureApp(
'file://C:/missing/FakeIOSApp.app.zip',
'.app'
).should.eventually.be.rejectedWith(/is not supported/);
await configureApp(
)).to.eventually.be.rejectedWith(/is not supported/);
await expect(configureApp(
`ftp://${TEST_HOST}:${port}/missing/FakeIOSApp.app.zip`,
'.app'
).should.eventually.be.rejectedWith(/is not supported/);
)).to.eventually.be.rejectedWith(/is not supported/);
});
it('should handle missing file in Windows path format', async function () {
await configureApp(
await expect(configureApp(
'C:\\missing\\FakeIOSApp.app.zip',
'.app'
).should.eventually.be.rejectedWith(/does not exist or is not accessible/);
});
it('should recognize zip mime types and unzip the downloaded file', async function () {
let newAppPath = await configureApp(
`${serverUrl}/FakeAndroidApp.asd?content-type=${encodeURIComponent('application/zip')}`,
'.apk'
);
newAppPath.should.contain('FakeAndroidApp.apk');
newAppPath.should.not.contain('.asd');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
});
it('should recognize zip mime types with parameter and unzip the downloaded file', async function () {
let newAppPath = await configureApp(
`${serverUrl}/FakeAndroidApp.asd?content-type=${encodeURIComponent(
'application/zip; parameter=value'
)}`,
'.apk'
);
newAppPath.should.contain('FakeAndroidApp.apk');
newAppPath.should.not.contain('.asd');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
});
it('should recognize zip mime types and unzip the downloaded file with query string', async function () {
let newAppPath = await configureApp(
`${serverUrl}/FakeAndroidApp.asd?content-type=${encodeURIComponent(
'application/zip'
)}&sv=abc&sr=def`,
'.apk'
);
newAppPath.should.contain('FakeAndroidApp.apk');
newAppPath.should.not.contain('.asd');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
)).to.eventually.be.rejectedWith(/does not exist or is not accessible/);
});
it('should treat an unknown mime type as an app', async function () {
let newAppPath = await configureApp(
`${serverUrl}/FakeAndroidApp.apk?content-type=${encodeURIComponent('application/bip')}`,
'.apk'
);
newAppPath.should.contain('.apk');
expect(newAppPath).to.contain('.apk');
let contents = await fs.readFile(newAppPath, 'utf8');
contents.should.eql('this is not really an apk\n');
expect(contents).to.eql('this is not really an apk\n');
});
});
});
@@ -11,12 +11,13 @@ describe('Execute Command Test', function () {
let sandbox;
let driver;
let httpServer;
let expect;
beforeEach(async function () {
const chai = await import('chai');
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
chai.should();
expect = chai.expect;
sandbox = createSandbox();
port = await getTestPort();
baseUrl = `http://${TEST_HOST}:${port}`;
@@ -43,13 +44,13 @@ describe('Execute Command Test', function () {
args,
});
res.status.should.eql(200);
res.data.should.have.property('value');
res.data.value.should.deep.equal({executed: script, args});
expect(res.status).to.eql(200);
expect(res.data).to.have.property('value');
expect(res.data.value).to.deep.equal({executed: script, args});
const events = await driver.getLogEvents();
const command = events.commands[0];
command.should.have.property('cmd', 'mobileActivateApp');
expect(command).to.have.property('cmd', 'mobileActivateApp');
});
});
@@ -9,6 +9,7 @@ describe('Websockets (e2e)', function () {
let baseServer;
let driver;
let port;
let expect;
const SESSION_ID = 'foo';
const WS_DATA = 'Hello';
@@ -16,7 +17,7 @@ describe('Websockets (e2e)', function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
driver = new FakeDriver();
driver.sessionId = SESSION_ID;
@@ -43,19 +44,19 @@ describe('Websockets (e2e)', function () {
const endpoint = `${DEFAULT_WS_PATHNAME_PREFIX}/hello`;
const timeout = 5000;
await baseServer.addWebSocketHandler(endpoint, wss);
_.keys(await baseServer.getWebSocketHandlers()).length.should.eql(1);
expect(_.keys(await baseServer.getWebSocketHandlers()).length).to.eql(1);
await new B((resolve, reject) => {
const client = new WebSocket(`ws://${TEST_HOST}:${port}${endpoint}`);
client.once('upgrade', (res) => {
try {
res.statusCode.should.eql(101);
expect(res.statusCode).to.eql(101);
} catch (e) {
reject(e);
}
});
client.once('message', (data) => {
const dataStr = _.isString(data) ? data : data.toString();
dataStr.should.eql(WS_DATA);
expect(dataStr).to.eql(WS_DATA);
resolve();
});
client.once('error', reject);
@@ -65,8 +66,8 @@ describe('Websockets (e2e)', function () {
);
});
(await baseServer.removeWebSocketHandler(endpoint)).should.be.true;
_.keys(await baseServer.getWebSocketHandlers()).length.should.eql(0);
expect(await baseServer.removeWebSocketHandler(endpoint)).to.be.true;
expect(_.keys(await baseServer.getWebSocketHandlers()).length).to.eql(0);
await new B((resolve, reject) => {
const client = new WebSocket(`ws://${TEST_HOST}:${port}${endpoint}`);
client.on('message', (data) =>
@@ -21,11 +21,12 @@ describe('server', function () {
let hwServer;
let port;
let sandbox;
let expect;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
port = await getTestPort(true);
@@ -64,26 +65,16 @@ describe('server', function () {
it('should start up with our middleware', async function () {
const {data} = await axios.get(`http://${TEST_HOST}:${port}/`);
data.should.eql('Hello World!');
});
it('should fix broken context type', async function () {
const {data} = await axios({
url: `http://${TEST_HOST}:${port}/python`,
headers: {
'user-agent': 'Python',
'content-type': 'application/x-www-form-urlencoded',
},
});
data.should.eql('application/json; charset=utf-8');
expect(data).to.eql('Hello World!');
});
it('should catch errors in the catchall', async function () {
await axios.get(`http://${TEST_HOST}:${port}/error`).should.be.rejected;
await expect(axios.get(`http://${TEST_HOST}:${port}/error`)).to.be.rejected;
});
it('should error if we try to start again on a port that is used', async function () {
await server({
await expect(server({
routeConfiguringFunction() {},
port,
}).should.be.rejectedWith(/EADDRINUSE/);
})).to.be.rejectedWith(/EADDRINUSE/);
});
it('should not wait for the server close connections before finishing closing', async function () {
const bodyPromise = (async () => {
@@ -98,22 +89,22 @@ describe('server', function () {
let before = Date.now();
await hwServer.close();
// expect slightly less than the request waited, since we paused above
(Date.now() - before).should.not.be.above(800);
expect(Date.now() - before).to.not.be.above(800);
await bodyPromise;
});
it('should error if we try to start on a bad hostname', async function () {
this.timeout(60000);
await server({
await expect(server({
routeConfiguringFunction: _.noop,
port,
hostname: 'lolcathost',
}).should.be.rejectedWith(/ENOTFOUND|EADDRNOTAVAIL|EAI_AGAIN/);
await server({
})).to.be.rejectedWith(/ENOTFOUND|EADDRNOTAVAIL|EAI_AGAIN/);
await expect(server({
routeConfiguringFunction: _.noop,
port,
hostname: '1.1.1.1',
}).should.be.rejectedWith(/EADDRNOTAVAIL/);
})).to.be.rejectedWith(/EADDRNOTAVAIL/);
});
});
@@ -122,6 +113,7 @@ describe('tls server', function () {
let port;
let certPath = 'certificate.cert';
let keyPath = 'certificate.key';
let expect;
const looseClient = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
@@ -132,7 +124,7 @@ describe('tls server', function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
try {
await generateCertificate(certPath, keyPath);
@@ -167,25 +159,26 @@ describe('tls server', function () {
it('should start up with our middleware', async function () {
const {data} = await looseClient.get(`https://${TEST_HOST}:${port}/`);
data.should.eql('Hello World!');
expect(data).to.eql('Hello World!');
});
it('should throw if untrusted', async function () {
await axios.get(`https://${TEST_HOST}:${port}/`).should.eventually.be.rejected;
await expect(axios.get(`https://${TEST_HOST}:${port}/`)).to.eventually.be.rejected;
});
it('should throw if not secure', async function () {
await axios.get(`http://${TEST_HOST}:${port}/`).should.eventually.be.rejected;
await expect(axios.get(`http://${TEST_HOST}:${port}/`)).to.eventually.be.rejected;
});
});
describe('server plugins', function () {
let hwServer;
let port;
let expect;
before(async function () {
const chai = await import('chai');
const chaisAsPromised = await import('chai-as-promised');
chai.use(chaisAsPromised.default);
chai.should();
expect = chai.expect;
port = await getTestPort(true);
});
@@ -217,14 +210,14 @@ describe('server plugins', function () {
],
});
let {data} = await axios.get(`http://${TEST_HOST}:${port}/plugin1`);
data.should.eql('res from plugin1 route');
expect(data).to.eql('res from plugin1 route');
({data} = await axios.get(`http://${TEST_HOST}:${port}/plugin2`));
data.should.eql('res from plugin2 route');
hwServer._updated_plugin1.should.be.true;
hwServer._updated_plugin2.should.be.true;
expect(data).to.eql('res from plugin2 route');
expect(hwServer._updated_plugin1).to.be.true;
expect(hwServer._updated_plugin2).to.be.true;
});
it('should pass on errors from the plugin updateServer method', async function () {
await server({
await expect(server({
routeConfiguringFunction: _.noop,
port,
serverUpdaters: [
@@ -232,6 +225,6 @@ describe('server plugins', function () {
throw new Error('ugh');
},
],
}).should.eventually.be.rejectedWith(/ugh/);
})).to.eventually.be.rejectedWith(/ugh/);
});
});
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.0.4](https://github.com/appium/appium/compare/@appium/base-plugin@3.0.3...@appium/base-plugin@3.0.4) (2025-11-12)
**Note:** Version bump only for package @appium/base-plugin
## [3.0.3](https://github.com/appium/appium/compare/@appium/base-plugin@3.0.2...@appium/base-plugin@3.0.3) (2025-10-08)
**Note:** Version bump only for package @appium/base-plugin
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "3.0.3",
"version": "3.0.4",
"description": "The base plugin used to create Appium plugins",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^10.1.0",
"@appium/support": "^7.0.2"
"@appium/base-driver": "^10.1.1",
"@appium/support": "^7.0.3"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.1.2](https://github.com/appium/appium/compare/@appium/docutils@2.1.1...@appium/docutils@2.1.2) (2025-11-12)
**Note:** Version bump only for package @appium/docutils
## [2.1.1](https://github.com/appium/appium/compare/@appium/docutils@2.1.0...@appium/docutils@2.1.1) (2025-10-08)
**Note:** Version bump only for package @appium/docutils
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "2.1.1",
"version": "2.1.2",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -48,7 +48,7 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "^7.0.2",
"@appium/support": "^7.0.3",
"chalk": "4.1.2",
"consola": "3.4.2",
"diff": "8.0.2",
@@ -57,8 +57,8 @@
"pkg-dir": "5.0.0",
"read-pkg": "5.2.0",
"source-map-support": "0.5.21",
"teen_process": "3.0.1",
"type-fest": "5.0.1",
"teen_process": "3.0.2",
"type-fest": "5.2.0",
"yaml": "2.8.1",
"yargs": "18.0.0",
"yargs-parser": "22.0.0"
+2 -2
View File
@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-git-revision-date-localized-plugin==1.4.7
mkdocs-material==9.6.21
mkdocs-git-revision-date-localized-plugin==1.5.0
mkdocs-material==9.6.23
mkdocs-redirects==1.2.2
mike==2.1.3
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.4](https://github.com/appium/appium/compare/@appium/driver-test-support@1.0.3...@appium/driver-test-support@1.0.4) (2025-11-12)
### Bug Fixes
* **driver-test-support:** fix test failures due to invalid syntax ([#21683](https://github.com/appium/appium/issues/21683)) ([b12a473](https://github.com/appium/appium/commit/b12a47349af2890b17edb055d6f5145686cbc170))
## [1.0.3](https://github.com/appium/appium/compare/@appium/driver-test-support@1.0.2...@appium/driver-test-support@1.0.3) (2025-10-08)
**Note:** Version bump only for package @appium/driver-test-support
+43 -37
View File
@@ -126,14 +126,12 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
/** @type {SessionHelpers['postCommand']} */
let postCommand;
let expect;
let should;
before(async function () {
const chai = await import('chai');
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
expect = chai.expect;
should = chai.should();
port = port ?? (await getTestPort());
defaultCaps = {...defaultCaps};
@@ -154,6 +152,11 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
describe('session handling', function () {
it('should handle idempotency while creating sessions', async function () {
// TODO: Fix this test for Node 24+
if (parseInt(process.versions.node.split('.')[0], 10) >= 24) {
this.skip();
}
// workaround for https://github.com/node-fetch/node-fetch/issues/1735
const httpAgent = new Agent({keepAlive: true});
@@ -175,14 +178,19 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
sessionIds.push(sessionId);
times++;
} while (times < 2);
should(_.uniq(sessionIds).length).equal(1);
expect(_.uniq(sessionIds)).to.have.lengthOf(1);
const {status, data} = await endSession(sessionIds[0]);
should(status).equal(200);
should.equal(data.value, null);
expect(status).to.equal(200);
expect(data.value).to.be.null;
});
it('should handle idempotency while creating parallel sessions', async function () {
// TODO: Fix this test for Node 24+
if (parseInt(process.versions.node.split('.')[0], 10) >= 24) {
this.skip();
}
// workaround for https://github.com/node-fetch/node-fetch/issues/1735
const httpAgent = new Agent({keepAlive: true});
@@ -207,11 +215,11 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
times++;
} while (times < 2);
const sessionIds = _.map(await B.all(reqs), 'sessionId');
should(_.uniq(sessionIds).length).equal(1);
expect(_.uniq(sessionIds)).to.have.lengthOf(1);
const {status, data} = await endSession(sessionIds[0]);
should(status).equal(200);
should.equal(data.value, null);
expect(status).to.equal(200);
expect(data.value).to.be.null;
});
it('should create session and retrieve a session id, then delete it', async function () {
@@ -221,21 +229,19 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
},
});
should(status).equal(200);
should.exist(data.value.sessionId);
should(data.value.capabilities.platformName).equal(defaultCaps.platformName);
should(data.value.capabilities.deviceName).equal(defaultCaps['appium:deviceName']);
expect(status).to.equal(200);
expect(data.value.sessionId).to.exist;
expect(data.value.capabilities.platformName).to.equal(defaultCaps.platformName);
expect(data.value.capabilities.deviceName).to.equal(defaultCaps['appium:deviceName']);
({status, data} = await endSession(/** @type {string} */ (d.sessionId)));
should(status).equal(200);
should.equal(data.value, null);
should.equal(d.sessionId, null);
expect(status).to.equal(200);
expect(data.value).to.be.null;
expect(d.sessionId).to.be.null;
});
});
it.skip('should throw NYI for commands not implemented', async function () {});
describe('command timeouts', function () {
let originalFindElement, originalFindElements;
@@ -268,7 +274,7 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
it('should set a default commandTimeout', async function () {
let newSession = await startTimeoutSession();
should(d.newCommandTimeoutMs).be.above(0);
expect(d.newCommandTimeoutMs).to.be.above(0);
await endSession(newSession.sessionId);
});
@@ -281,10 +287,10 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
});
await B.delay(400);
const value = await getSession(sessionId);
should.equal(value.error, 'invalid session id');
should.equal(d.sessionId, null);
expect(value.error).to.equal('invalid session id');
expect(d.sessionId).to.be.null;
const resp = (await endSession(newSession.sessionId)).data.value;
should.equal(resp?.error, 'invalid session id');
expect(resp?.error).to.equal('invalid session id');
});
it('should not timeout with commandTimeout of false', async function () {
@@ -294,8 +300,8 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
using: 'name',
value: 'foo',
});
should(Date.now() - start).be.above(150);
should(value).eql(['foo']);
expect(Date.now() - start).to.be.above(150);
expect(value).to.eql(['foo']);
await endSession(newSession.sessionId);
});
@@ -309,9 +315,9 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
});
await B.delay(400);
const value = await getSession(/** @type {string} */ (d.sessionId));
should(value.platformName).equal(defaultCaps.platformName);
expect(value.platformName).to.equal(defaultCaps.platformName);
const resp = (await endSession(newSession.sessionId)).data.value;
should.equal(resp, null);
expect(resp).to.be.null;
d.newCommandTimeoutMs = 60 * 1000;
});
@@ -328,24 +334,24 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
});
await B.delay(400);
const value = await getSession(/** @type {string} */ (sessionId));
should(/** @type {string} */ (value.error)).equal('invalid session id');
should.equal(d.sessionId, null);
expect(/** @type {string} */ (value.error)).to.equal('invalid session id');
expect(d.sessionId).to.be.null;
const resp = (await endSession(newSession.sessionId)).data.value;
should(/** @type {string} */ (/** @type { {error: string} } */ (resp).error)).equal(
expect(/** @type {string} */ (/** @type { {error: string} } */ (resp).error)).to.equal(
'invalid session id'
);
});
it('should not have a timer running before or after a session', async function () {
// @ts-expect-error
should.not.exist(d.noCommandTimer);
expect(d.noCommandTimer).to.be.null;
let newSession = await startTimeoutSession(0.25);
should(newSession.sessionId).equal(d.sessionId);
expect(newSession.sessionId).to.equal(d.sessionId);
// @ts-expect-error
should.exist(d.noCommandTimer);
expect(d.noCommandTimer).to.exist;
await endSession(newSession.sessionId);
// @ts-expect-error
should.not.exist(d.noCommandTimer);
expect(d.noCommandTimer).to.be.null;
});
});
@@ -354,16 +360,16 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
d.settings = new DeviceSettings({ignoreUnimportantViews: false});
});
it('should be able to get settings object', function () {
d.settings.getSettings().ignoreUnimportantViews.should.be.false;
expect(d.settings.getSettings().ignoreUnimportantViews).to.be.false;
});
it('should not reject when `updateSettings` method is not provided', async function () {
await should(d.settings.update({ignoreUnimportantViews: true})).not.be.rejected();
await expect(d.settings.update({ignoreUnimportantViews: true})).to.not.be.rejected;
});
it('should reject for invalid update object', async function () {
await should(
await expect(
// @ts-expect-error
d.settings.update('invalid json')
).be.rejectedWith('JSON');
).to.be.rejectedWith('JSON');
});
});
@@ -399,7 +405,7 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
});
d.startUnexpectedShutdown(new Error('Crashytimes'));
const value = await reqPromise;
value.message.should.contain('Crashytimes');
expect(value.message).to.contain('Crashytimes');
await shutdownEventPromise;
});
});
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "1.0.3",
"version": "1.0.4",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -41,8 +41,8 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"@appium/types": "^1.1.0",
"axios": "1.12.2",
"@appium/types": "^1.1.1",
"axios": "1.13.2",
"bluebird": "3.7.2",
"chai": "6.2.0",
"chai-as-promised": "8.0.2",
@@ -50,7 +50,7 @@
"lodash": "4.17.21",
"sinon": "21.0.0",
"source-map-support": "0.5.21",
"type-fest": "5.0.1"
"type-fest": "5.2.0"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0",
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@2.0.3...@appium/eslint-config-appium-ts@2.0.4) (2025-11-12)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
## [2.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@2.0.2...@appium/eslint-config-appium-ts@2.0.3) (2025-10-08)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
@@ -1,7 +1,7 @@
{
"name": "@appium/eslint-config-appium-ts",
"type": "module",
"version": "2.0.3",
"version": "2.0.4",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
"eslint",
@@ -29,14 +29,14 @@
"test:smoke": "exit 0"
},
"dependencies": {
"@eslint/compat": "1.4.0",
"@eslint/js": "9.37.0",
"@eslint/compat": "1.4.1",
"@eslint/js": "9.39.1",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-mocha": "10.5.0",
"eslint-plugin-promise": "7.2.1",
"typescript-eslint": "8.46.0"
"typescript-eslint": "8.46.4"
},
"peerDependencies": {
"eslint": "^9.0.0"
@@ -46,7 +46,7 @@ describe('FakeDriver - via HTTP', function () {
should.exist(driver.sessionId);
driver.sessionId.should.be.a('string');
await deleteSession(driver);
await driver.getTitle().should.eventually.be.rejectedWith(/terminated/);
await driver.getTitle().should.eventually.be.rejected;
});
});
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.0.4](https://github.com/appium/appium/compare/@appium/fake-plugin@4.0.3...@appium/fake-plugin@4.0.4) (2025-11-12)
**Note:** Version bump only for package @appium/fake-plugin
## [4.0.3](https://github.com/appium/appium/compare/@appium/fake-plugin@4.0.2...@appium/fake-plugin@4.0.3) (2025-10-08)
**Note:** Version bump only for package @appium/fake-plugin
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-plugin",
"version": "4.0.3",
"version": "4.0.4",
"description": "A fake Appium plugin",
"keywords": [
"automation",
@@ -39,8 +39,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-plugin": "^3.0.3",
"@appium/support": "^7.0.2",
"@appium/base-plugin": "^3.0.4",
"@appium/support": "^7.0.3",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"source-map-support": "0.5.21"
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.0.3](https://github.com/appium/appium/compare/@appium/images-plugin@4.0.2...@appium/images-plugin@4.0.3) (2025-11-12)
**Note:** Version bump only for package @appium/images-plugin
## [4.0.2](https://github.com/appium/appium/compare/@appium/images-plugin@4.0.1...@appium/images-plugin@4.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/images-plugin
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/images-plugin",
"version": "4.0.2",
"version": "4.0.3",
"description": "Plugin for working with images and image elements in Appium",
"keywords": [
"automation",
@@ -39,11 +39,11 @@
"test:unit": "mocha \"./test/unit/**/*.spec.*js\""
},
"dependencies": {
"@appium/opencv": "^4.0.2",
"@appium/support": "^7.0.2",
"@appium/opencv": "^4.0.3",
"@appium/support": "^7.0.3",
"lodash": "4.17.21",
"lru-cache": "11.2.2",
"sharp": "0.34.4",
"sharp": "0.34.5",
"source-map-support": "0.5.21"
},
"peerDependencies": {
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.0.3](https://github.com/appium/appium/compare/@appium/opencv@4.0.2...@appium/opencv@4.0.3) (2025-11-12)
**Note:** Version bump only for package @appium/opencv
## [4.0.2](https://github.com/appium/appium/compare/@appium/opencv@4.0.1...@appium/opencv@4.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/opencv
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/opencv",
"version": "4.0.2",
"version": "4.0.3",
"description": "OpenCV-related helper library",
"keywords": [
"automation",
@@ -44,7 +44,7 @@
"bluebird": "3.7.2",
"lodash": "4.17.21",
"opencv-bindings": "4.5.5",
"sharp": "0.34.4",
"sharp": "0.34.5",
"source-map-support": "0.5.21"
},
"engines": {
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.3](https://github.com/appium/appium/compare/@appium/plugin-test-support@1.0.2...@appium/plugin-test-support@1.0.3) (2025-11-12)
**Note:** Version bump only for package @appium/plugin-test-support
## [1.0.2](https://github.com/appium/appium/compare/@appium/plugin-test-support@1.0.1...@appium/plugin-test-support@1.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/plugin-test-support
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/plugin-test-support",
"version": "1.0.2",
"version": "1.0.3",
"description": "Testing utilities for Appium plugins",
"keywords": [
"automation",
@@ -41,11 +41,11 @@
"test:unit": "exit 0"
},
"dependencies": {
"@appium/types": "^1.1.0",
"@appium/types": "^1.1.1",
"get-port": "5.1.1",
"log-symbols": "4.1.0",
"source-map-support": "0.5.21",
"teen_process": "3.0.1"
"teen_process": "3.0.2"
},
"peerDependencies": {
"appium": "^3.0.0-beta.0",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.3](https://github.com/appium/appium/compare/@appium/storage-plugin@1.0.2...@appium/storage-plugin@1.0.3) (2025-11-12)
**Note:** Version bump only for package @appium/storage-plugin
## [1.0.2](https://github.com/appium/appium/compare/@appium/storage-plugin@1.0.1...@appium/storage-plugin@1.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/storage-plugin
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/storage-plugin",
"version": "1.0.2",
"version": "1.0.3",
"description": "Server-side storage plugin for Appium",
"keywords": [
"automation",
@@ -41,12 +41,12 @@
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.*js\""
},
"dependencies": {
"@appium/support": "^7.0.2",
"@appium/support": "^7.0.3",
"async-lock": "1.4.1",
"bluebird": "3.7.2",
"lodash": "4.17.21",
"lru-cache": "11.2.2",
"rimraf": "6.0.1",
"rimraf": "6.1.0",
"source-map-support": "0.5.21",
"ws": "8.18.3"
},
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.0.3](https://github.com/appium/appium/compare/@appium/support@7.0.2...@appium/support@7.0.3) (2025-11-12)
### Bug Fixes
* **base-driver:** Use shouldUpgradeCallback for proper upgrades processing ([#21706](https://github.com/appium/appium/issues/21706)) ([fc273da](https://github.com/appium/appium/commit/fc273da0ef04d3362f10c710f7c8692d7a03fd21))
* **support:** Properly handle moving to another volume ([#21672](https://github.com/appium/appium/issues/21672)) ([5d62688](https://github.com/appium/appium/commit/5d62688e8b5244ca432b5f276b23062ddb757b6b))
## [7.0.2](https://github.com/appium/appium/compare/@appium/support@7.0.1...@appium/support@7.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/support
+11 -1
View File
@@ -172,7 +172,17 @@ const fs = {
}
await this.rimraf(dst);
}
await fsPromises.rename(src, dst);
try {
await fsPromises.rename(src, dst);
} catch (err) {
// Handle cross-device link error by falling back to copy-and-delete
if (err.code === 'EXDEV') {
await this.copyFile(String(src), String(dst));
await this.rimraf(src);
} else {
throw err;
}
}
};
/** @type {import('fs').Stats} */
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/support",
"version": "7.0.2",
"version": "7.0.3",
"description": "Support libs used across Appium packages",
"keywords": [
"automation",
@@ -43,10 +43,10 @@
"dependencies": {
"@appium/logger": "^2.0.2",
"@appium/tsconfig": "^1.1.0",
"@appium/types": "^1.1.0",
"@appium/types": "^1.1.1",
"@colors/colors": "1.6.0",
"archiver": "7.0.1",
"axios": "1.12.2",
"axios": "1.13.2",
"base64-stream": "1.0.0",
"bluebird": "3.7.2",
"bplist-creator": "0.1.1",
@@ -71,14 +71,14 @@
"shell-quote": "1.8.3",
"source-map-support": "0.5.21",
"supports-color": "8.1.1",
"teen_process": "3.0.1",
"type-fest": "5.0.1",
"teen_process": "3.0.2",
"type-fest": "5.2.0",
"uuid": "13.0.0",
"which": "5.0.0",
"yauzl": "3.2.0"
},
"optionalDependencies": {
"sharp": "0.34.4"
"sharp": "0.34.5"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+24
View File
@@ -73,6 +73,30 @@ describe('fs', function () {
await fs.mv(srcPath, dstPath);
(await fs.readFile(dstPath)).toString().should.eql('bar');
});
it('should handle cross-device move by falling back to copy-and-delete', async function () {
const srcPath = path.join(srcRoot, 'src.file');
await fs.writeFile(srcPath, Buffer.from('bar'));
const dstPath = path.join(dstRoot, path.basename(srcPath));
// Mock fsPromises.rename to simulate EXDEV error
const originalRename = fs.rename;
fs.rename = async () => {
const err = new Error('cross-device link not permitted');
err.code = 'EXDEV';
throw err;
};
try {
await fs.mv(srcPath, dstPath);
(await fs.exists(dstPath)).should.be.true;
(await fs.exists(srcPath)).should.be.false;
(await fs.readFile(dstPath)).toString().should.eql('bar');
} finally {
// Restore original function
fs.rename = originalRename;
}
});
});
describe('isExecutable()', function () {
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.0.3](https://github.com/appium/appium/compare/@appium/test-support@4.0.2...@appium/test-support@4.0.3) (2025-11-12)
**Note:** Version bump only for package @appium/test-support
## [4.0.2](https://github.com/appium/appium/compare/@appium/test-support@4.0.1...@appium/test-support@4.0.2) (2025-10-08)
**Note:** Version bump only for package @appium/test-support
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/test-support",
"version": "4.0.2",
"version": "4.0.3",
"description": "A collection of test utilities used across Appium packages",
"keywords": [
"automation",
@@ -45,7 +45,7 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "^7.0.2",
"@appium/support": "^7.0.3",
"@colors/colors": "1.6.0",
"bluebird": "3.7.2",
"lodash": "4.17.21",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.1](https://github.com/appium/appium/compare/@appium/types@1.1.0...@appium/types@1.1.1) (2025-11-12)
**Note:** Version bump only for package @appium/types
## [1.1.0](https://github.com/appium/appium/compare/@appium/types@1.0.1...@appium/types@1.1.0) (2025-10-08)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/types",
"version": "1.1.0",
"version": "1.1.1",
"description": "Various type declarations used across Appium",
"keywords": [
"automation",
@@ -41,7 +41,7 @@
"@appium/logger": "^2.0.2",
"@appium/schema": "^1.0.0",
"@appium/tsconfig": "^1.1.0",
"type-fest": "5.0.1"
"type-fest": "5.2.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.2](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@2.0.1...@appium/universal-xml-plugin@2.0.2) (2025-11-12)
**Note:** Version bump only for package @appium/universal-xml-plugin
## [2.0.1](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@2.0.0...@appium/universal-xml-plugin@2.0.1) (2025-10-08)
**Note:** Version bump only for package @appium/universal-xml-plugin
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/universal-xml-plugin",
"version": "2.0.1",
"version": "2.0.2",
"description": "Appium plugin for making XML source and XPath queries the same across iOS and Android",
"keywords": [
"automation",
@@ -38,7 +38,7 @@
},
"dependencies": {
"@xmldom/xmldom": "0.9.8",
"fast-xml-parser": "5.3.0",
"fast-xml-parser": "5.3.1",
"lodash": "4.17.21",
"source-map-support": "0.5.21",
"xpath": "0.0.34"
+345
View File
@@ -0,0 +1,345 @@
import {log, performApiRequest} from './crowdin-common.mjs';
import {net, fs, tempDir} from '@appium/support';
import {waitForCondition} from 'asyncbox';
const REPORT_TIMEOUT_MS = 1000 * 60 * 5; // 5 minutes
const REPORT_STATUS = {
finished: 'finished',
created: 'created',
inProgress: 'in_progress',
};
/**
* Get the date range for the last calendar month
* @returns {{from: string, to: string}}
*/
function getLastMonthDateRange() {
const now = new Date();
// First day of last month at 00:00:00 UTC
const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0));
// Last day of last month at 23:59:59 UTC
const to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 0, 23, 59, 59));
return {
from: from.toISOString(), // Full ISO 8601 format with UTC: YYYY-MM-DDTHH:mm:ss.sssZ
to: to.toISOString(),
};
}
/**
* Generate a translation activity report
* @param {string} from - Start date in ISO 8601 format (e.g., 2025-09-01T00:00:00.000Z)
* @param {string} to - End date in ISO 8601 format (e.g., 2025-09-30T23:59:59.000Z)
* @returns {Promise<string>} Report ID
*/
async function generateReport(from, to) {
log.info(`Generating translation report for ${from} to ${to}`);
const {data: reportData} = await performApiRequest('/reports', {
method: 'POST',
payload: {
name: 'top-members',
schema: {
unit: 'strings',
format: 'json',
dateFrom: from,
dateTo: to,
},
},
});
return reportData.identifier;
}
/**
* Check report generation status
* @param {string} reportId
* @returns {Promise<{status: string, progress: number}>}
*/
async function checkReportStatus(reportId) {
const {data: statusData} = await performApiRequest(`/reports/${reportId}`);
return {
status: statusData.status,
progress: statusData.progress,
};
}
/**
* Wait for report to be ready and download it
* @param {string} reportId - The unique identifier of the generated report
* @returns {Promise<CrowdinReportData>} Parsed report data from Crowdin
*/
async function downloadReport(reportId) {
log.info(`Waiting up to ${REPORT_TIMEOUT_MS / 1000}s for report ${reportId} to finish`);
await waitForCondition(
async () => {
const {status, progress} = await checkReportStatus(reportId);
log.debug(`Report status: ${status}, progress: ${progress}%`);
switch (status) {
case REPORT_STATUS.finished:
return true;
case REPORT_STATUS.inProgress:
case REPORT_STATUS.created:
return false;
default:
throw new Error(`Report generation failed with status '${status}'`);
}
},
{
waitMs: REPORT_TIMEOUT_MS,
intervalMs: 2000,
},
);
const {data: downloadData} = await performApiRequest(`/reports/${reportId}/download`);
log.info('Downloading report data');
// Download the report file
const tmpFile = await tempDir.path({prefix: 'crowdin-report', suffix: '.json'});
try {
await net.downloadFile(downloadData.url, tmpFile, {isMetered: false});
const content = await fs.readFile(tmpFile, 'utf8');
return JSON.parse(content);
} finally {
await fs.rimraf(tmpFile);
}
}
/**
* Process report data and group by language and translator
* @param {CrowdinReportData} reportData - Raw report data from Crowdin API
* @returns {ProcessedStats} Grouped statistics by language and user
*/
function processReportData(reportData) {
const stats = {};
if (reportData.data && Array.isArray(reportData.data)) {
for (const record of reportData.data) {
const user = record.user?.username || record.user?.fullName || 'Unknown';
const translated = record.translated || 0;
const approved = record.approved || 0;
// Each record has a languages array
const languages = record.languages || [];
if (languages.length === 0) {
// If no languages specified, use "Unknown"
languages.push({name: 'Unknown'});
}
// Add this user's stats to each language they contributed to
for (const lang of languages) {
const languageName = lang.name || 'Unknown';
if (!stats[languageName]) {
stats[languageName] = {};
}
if (!stats[languageName][user]) {
stats[languageName][user] = {translated: 0, approved: 0};
}
stats[languageName][user].translated += translated;
stats[languageName][user].approved += approved;
}
}
}
return stats;
}
/**
* Get project details
* @returns {Promise<{name: string, id: number}>}
*/
async function getProjectInfo() {
const {data: projectData} = await performApiRequest('', {
method: 'GET',
});
return {
name: projectData.name,
id: projectData.id,
};
}
/**
* Format statistics as Slack message blocks
* @param {ProcessedStats} stats - Grouped statistics by language and user
* @param {string} projectName - Project name
* @param {string} from - Start date in ISO 8601 format
* @param {string} to - End date in ISO 8601 format
* @param {string} generatedAt - Generation timestamp in ISO 8601 format
* @returns {object} Slack message payload with blocks
*/
function formatSlackMessage(stats, projectName, from, to, generatedAt) {
const monthName = new Date(from).toLocaleString('en-US', { month: 'long', year: 'numeric' });
// Format dates as YYYY-MM-DD for display
const fromDate = from.split('T')[0];
const toDate = to.split('T')[0];
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: `📊 Crowdin Translation Stats for ${monthName}`,
emoji: true,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Project:* ${projectName}\n*Period:* ${fromDate} to ${toDate}`,
},
},
{
type: 'divider',
},
];
if (Object.keys(stats).length === 0) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: '❌ No translation activity found for this period.',
},
});
} else {
// Slack section text is limited to 3000 characters. Keep a safety margin.
const MAX_SECTION_CHARS = 2900;
for (const [language, users] of Object.entries(stats)) {
const userEntries = Object.entries(users)
.filter(([, counts]) => counts.translated > 0 || counts.approved > 0)
.sort(([, a], [, b]) => (b.translated + b.approved) - (a.translated + a.approved));
if (userEntries.length === 0) {
continue;
}
const totalTranslated = userEntries.reduce((sum, [, u]) => sum + u.translated, 0);
const totalApproved = userEntries.reduce((sum, [, u]) => sum + u.approved, 0);
const header = `*${language}* (Total: ${totalTranslated} translated, ${totalApproved} approved)`;
// Build bullet lines
const lines = userEntries.map(([user, counts]) => `• *${user}*: ${counts.translated} translated, ${counts.approved} approved`);
// Chunk lines so each section stays under limit
let current = header;
let buf = [];
const flush = () => {
const text = buf.length ? `${current}\n${buf.join('\n')}` : current;
blocks.push({
type: 'section',
text: {type: 'mrkdwn', text},
});
buf = [];
current = header; // next chunk keeps the same header for clarity
};
for (const line of lines) {
const tentative = buf.length ? `${current}\n${buf.join('\n')}\n${line}` : `${current}\n${line}`;
if (tentative.length > MAX_SECTION_CHARS) {
flush();
}
buf.push(line);
}
flush();
}
}
blocks.push(
{
type: 'divider',
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Generated on ${generatedAt.split('T')[0]} | <https://crowdin.com|View on Crowdin>`,
},
],
},
);
return {blocks};
}
async function main() {
const dateRange = getLastMonthDateRange();
log.info(`Collecting translation statistics for ${dateRange.from} to ${dateRange.to}`);
// Get project info
const projectInfo = await getProjectInfo();
log.info(`Project: ${projectInfo.name} (ID: ${projectInfo.id})`);
// Generate and download report
const reportId = await generateReport(dateRange.from, dateRange.to);
const reportData = await downloadReport(reportId);
// Process the data
const stats = processReportData(reportData);
const generatedAt = new Date().toISOString();
// Output Slack-formatted message to stdout
const slackMessage = formatSlackMessage(
stats,
projectInfo.name,
dateRange.from,
dateRange.to,
generatedAt
);
// eslint-disable-next-line no-console
console.log(JSON.stringify(slackMessage, null, 2));
log.info('All done!');
}
(async () => await main())();
// Type definitions
/**
* @typedef {Object} CrowdinUser
* @property {string} id - User ID
* @property {string} username - Username
* @property {string} fullName - Full name
* @property {string} avatarUrl - Avatar URL
* @property {string} joined - Join date
*/
/**
* @typedef {Object} CrowdinLanguage
* @property {string} id - Language ID (e.g., "zh-CN")
* @property {string} name - Language name (e.g., "Chinese Simplified")
*/
/**
* @typedef {Object} CrowdinReportRecord
* @property {CrowdinUser} user - User information
* @property {CrowdinLanguage[]} languages - Languages the user contributed to
* @property {number} translated - Number of strings translated
* @property {number} approved - Number of strings approved
* @property {number} voted - Number of votes cast
* @property {number} positiveVotes - Number of positive votes received
* @property {number} negativeVotes - Number of negative votes received
* @property {number} winning - Number of winning translations
*/
/**
* @typedef {Object} CrowdinReportData
* @property {CrowdinReportRecord[]} data - Array of report records
*/
/**
* @typedef {Object} UserStats
* @property {number} translated - Number of strings translated
* @property {number} approved - Number of strings approved
*/
/**
* @typedef {Record<string, Record<string, UserStats>>} ProcessedStats
* Language name mapped to username mapped to stats
*/
+364
View File
@@ -0,0 +1,364 @@
import axios from 'axios';
import {logger} from '@appium/support';
const log = logger.getLogger('Contributions');
const GITHUB_API_BASE = 'https://api.github.com';
const GITHUB_ORG = 'appium';
const MAX_PAGE_LIMIT = 20;
const ITEMS_PER_PAGE = 100;
const MAX_TITLE_LENGTH = 120;
/**
* Extract date part from ISO string (removes time component)
* @param {string} isoString - ISO date string
* @returns {string} Date part only (YYYY-MM-DD)
*/
function extractDatePart(isoString) {
return isoString.split('T')[0];
}
// Internal exclusion list - usernames to exclude from reports
const INTERNAL_EXCLUSION_LIST = [
'dependabot',
'dependabot[bot]',
'renovate',
'renovate[bot]',
'appium-ci',
'appium-bot',
'github-actions[bot]',
'pre-commit-ci[bot]',
'codecov[bot]',
'sonarcloud[bot]',
'sonarcloud[bot]',
'greenkeeper[bot]',
'mergify[bot]',
'cla-bot',
'claassistant[bot]',
'semantic-release-bot',
'semantic-release[bot]',
'allcontributors[bot]',
'imgbot[bot]',
'imgbot',
'stale[bot]',
'stale',
'welcome[bot]',
'welcome',
'invalid-email-address',
'invalid-email',
'noreply',
'noreply@github.com',
'jlipps',
'mykola-mokhnach',
'kazucocoa',
'saikrishna321',
'srinivasantarget',
'eglitise',
'navin772',
'harsha509',
'Delta456',
];
/**
* Sort PRs by author login (asc), then by merged date (asc)
* @param {GitHubPullRequest[]} pullRequests
* @returns {GitHubPullRequest[]}
*/
function sortPullRequests(pullRequests) {
return [...pullRequests].sort((a, b) => {
const aAuthor = (a.author?.login || '').toLowerCase();
const bAuthor = (b.author?.login || '').toLowerCase();
if (aAuthor < bAuthor) {
return -1;
}
if (aAuthor > bAuthor) {
return 1;
}
// If authors are equal, sort by merged date
const aMerged = a.merged_at ? new Date(a.merged_at).getTime() : 0;
const bMerged = b.merged_at ? new Date(b.merged_at).getTime() : 0;
return aMerged - bMerged;
});
}
/**
* Get the date range for the last calendar month
* @returns {{from: string, to: string}}
*/
function getLastMonthDateRange() {
const now = new Date();
// First day of last month at 00:00:00 UTC
const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0));
// Last day of last month at 23:59:59 UTC
const to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 0, 23, 59, 59));
return {
from: from.toISOString(),
to: to.toISOString(),
};
}
/**
* Make request to GitHub API (authenticated or unauthenticated)
* @param {string} endpoint - API endpoint
* @param {string|null} token - GitHub token (optional)
* @returns {Promise<{data: any, headers: any}>}
*/
async function makeGitHubRequest(endpoint, token) {
const url = `${GITHUB_API_BASE}${endpoint}`;
const headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'appium-contrib-stats',
};
// Add authorization header only if token is provided
if (token) {
headers.Authorization = `token ${token}`;
}
const response = await axios({
method: 'GET',
url,
headers,
});
return {data: response.data, headers: response.headers};
}
/**
* Get merged pull requests from the Appium organization for the specified date range using GitHub search API
* @param {Object} dateRange - Date range object with from and to properties
* @param {string} dateRange.from - Start date in ISO format
* @param {string} dateRange.to - End date in ISO format
* @param {string|null} [token] - GitHub token (optional)
* @returns {Promise<GitHubPullRequest[]>} Array of pull request objects
*/
async function getMergedPullRequestsFromSearch(dateRange, token) {
/** @type {GitHubPullRequest[]} */
const pullRequests = [];
let page = 1;
const perPage = ITEMS_PER_PAGE;
// Convert ISO dates to GitHub's expected format (YYYY-MM-DD)
const fromDate = extractDatePart(dateRange.from);
const toDate = extractDatePart(dateRange.to);
// Build search query for merged pull requests in the appium organization
// Using GitHub's search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests
const excludedAuthors = INTERNAL_EXCLUSION_LIST.map((author) => `-author:${author}`).join(' ');
const searchQuery = `org:${GITHUB_ORG} is:pr is:merged merged:${fromDate}..${toDate} ${excludedAuthors}`;
while (true) {
// Rely on local sorting; omit API-side sort/order for simplicity
const endpoint = `/search/issues?q=${encodeURIComponent(searchQuery)}&page=${page}&per_page=${perPage}`;
const {data} = await makeGitHubRequest(endpoint, token);
if (!data.items?.length) {
break;
}
// Process the search results (pull requests)
pullRequests.push(...data.items.map((pr) => ({
sha: pr.merge_commit_sha,
author: pr.user,
created_at: pr.created_at,
merged_at: pr.merged_at || pr.closed_at, // Use closed_at as fallback if merged_at is null
commit: {
message: pr.title, // Use PR title as the commit message
},
html_url: pr.html_url,
repository: pr.repository_url.split('/').pop(), // Extract repo name from URL
})));
page++;
// Safety check
if (page > MAX_PAGE_LIMIT) {
log.warn(`Reached maximum page limit (${MAX_PAGE_LIMIT}) for PR search`);
break;
}
}
return pullRequests;
}
/**
* Format GitHub pull request data as Slack message blocks
* @param {GitHubPullRequest[]} pullRequests - Array of pull request objects
* @param {string} from - Start date in ISO 8601 format
* @param {string} to - End date in ISO 8601 format
* @param {string} generatedAt - Generation timestamp in ISO 8601 format
* @returns {object} Slack message payload with blocks
*/
function formatSlackMessage(pullRequests, from, to, generatedAt) {
const monthName = new Date(from).toLocaleString('en-US', { month: 'long', year: 'numeric' });
const fromDate = extractDatePart(from);
const toDate = extractDatePart(to);
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: `🚀 GitHub Contribution Report for ${monthName}`,
emoji: true,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Organization:* @${GITHUB_ORG}\n*Period:* ${fromDate} to ${toDate}\n*Total Merged Pull Requests:* ${pullRequests.length}`,
},
},
{
type: 'divider',
},
];
if (pullRequests.length === 0) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: '❌ No merged pull requests found for this period.',
},
});
} else {
// Create simple markdown rows with links
const rowLines = pullRequests.map((pr, index) => {
// Format created date (when PR was created)
const createdDate = new Date(pr.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
// Format merged date (when PR was merged)
const mergedDate = new Date(pr.merged_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
const authorName = pr.author?.login || 'Unknown';
const authorUrl = pr.author?.html_url || `https://github.com/${authorName}`;
// Use pull request title directly
let prTitle = pr.commit.message; // This is the PR title from our data mapping
if (prTitle.length > MAX_TITLE_LENGTH) {
prTitle = prTitle.substring(0, MAX_TITLE_LENGTH - 1) + '…';
}
// Construct repository URL
const repoUrl = `https://github.com/${GITHUB_ORG}/${pr.repository}`;
// Format as simple markdown row with Slack-formatted links
// Column order changed to: index • author • title • repo • dates
return `${index + 1} • <${authorUrl}|${authorName}> • <${pr.html_url}|${prTitle}> • <${repoUrl}|${pr.repository}> • Created: ${createdDate} • Merged: ${mergedDate}`;
});
// Slack section text has a 3000 character limit. Keep under ~2900 to be safe.
const MAX_SECTION_CHARS = 2900;
let currentLines = [];
let currentLen = 0;
const flushSection = () => {
if (currentLines.length === 0) {
return;
}
const content = currentLines.join('\n');
blocks.push({
type: 'section',
text: {type: 'mrkdwn', text: content},
});
currentLines = [];
currentLen = 0;
};
for (const line of rowLines) {
const addLen = line.length + 1; // plus newline
if (currentLen + addLen > MAX_SECTION_CHARS) {
flushSection();
}
currentLines.push(line);
currentLen += addLen;
}
flushSection();
}
blocks.push(
{
type: 'divider',
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Generated on ${extractDatePart(generatedAt)} | <https://github.com/${GITHUB_ORG}|View Organization>`,
},
],
},
);
return {blocks};
}
async function main() {
const token = process.env.GITHUB_TOKEN;
if (!token) {
log.warn('GITHUB_TOKEN not provided - using unauthenticated requests');
}
const dateRange = getLastMonthDateRange();
log.info(`Collecting GitHub contribution statistics for ${dateRange.from} to ${dateRange.to}`);
// Get merged pull requests using GitHub search API
log.info('Fetching merged pull requests using GitHub search API...');
const allPullRequests = await getMergedPullRequestsFromSearch(dateRange, token);
log.info(`Found ${allPullRequests.length} merged pull requests across all repositories`);
const generatedAt = new Date().toISOString();
const sortedPullRequests = sortPullRequests(allPullRequests);
// Output Slack-formatted message to stdout
const slackMessage = formatSlackMessage(
sortedPullRequests,
dateRange.from,
dateRange.to,
generatedAt
);
// eslint-disable-next-line no-console
console.log(JSON.stringify(slackMessage, null, 2));
log.info('All done!');
}
(async () => await main())();
// Type definitions
/**
* @typedef {Object} GitHubUser
* @property {string} login - Username
* @property {number} id - User ID
* @property {string} html_url - Profile URL
* @property {string} avatar_url - Avatar URL
*/
/**
* @typedef {Object} GitHubCommitAuthor
* @property {string} name - Author name
* @property {string} email - Author email
* @property {string} date - Commit date
*/
/**
* @typedef {Object} GitHubPullRequest
* @property {string} sha - Merge commit SHA
* @property {GitHubUser} author - Author user object
* @property {GitHubUser} committer - Committer user object
* @property {Object} commit - Commit details
* @property {GitHubCommitAuthor} commit.author - Commit author details
* @property {GitHubCommitAuthor} commit.committer - Commit committer details
* @property {string} commit.message - Pull request title
* @property {string} html_url - Pull request URL
* @property {string} repository - Repository name
*/