mirror of
https://github.com/appium/appium.git
synced 2026-11-01 23:00:54 +08:00
Compare commits
127
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9a6b834bc | ||
|
|
297f4995f8 | ||
|
|
f42374eb6d | ||
|
|
d6c70f2f52 | ||
|
|
e35bc50454 | ||
|
|
63e28a014c | ||
|
|
e9b50225dc | ||
|
|
e9426fcfec | ||
|
|
5ab2fea576 | ||
|
|
377e9825de | ||
|
|
116c9a66e2 | ||
|
|
04b542cfcd | ||
|
|
c39be81d04 | ||
|
|
efc847380a | ||
|
|
cf7b4d84af | ||
|
|
5a3666f653 | ||
|
|
5790924bf8 | ||
|
|
39b7cd5084 | ||
|
|
fd912346fa | ||
|
|
1d5070e891 | ||
|
|
42953b2add | ||
|
|
d0f479f7a8 | ||
|
|
e67890f463 | ||
|
|
ff46dfa814 | ||
|
|
afc7e34402 | ||
|
|
eef4e3447a | ||
|
|
d6c42e99c0 | ||
|
|
6cf14802b7 | ||
|
|
d5090155ad | ||
|
|
591d15bc5e | ||
|
|
00dbd28190 | ||
|
|
b808dc866d | ||
|
|
d866bddc15 | ||
|
|
825833d1ad | ||
|
|
bb11d1cfe9 | ||
|
|
11c6aaae41 | ||
|
|
822206be20 | ||
|
|
374c85e22a | ||
|
|
a11a6ede33 | ||
|
|
41eab2c938 | ||
|
|
296c29ad84 | ||
|
|
07f9d41bd2 | ||
|
|
d8b633406a | ||
|
|
b9d339eaf3 | ||
|
|
1ea309814a | ||
|
|
19ff931bc0 | ||
|
|
9d2469fdf3 | ||
|
|
47a4ddd2a3 | ||
|
|
a1723231be | ||
|
|
f4a254f204 | ||
|
|
22b943dff1 | ||
|
|
19eaaf5764 | ||
|
|
55805390b5 | ||
|
|
e94275c229 | ||
|
|
f795fe6056 | ||
|
|
bbcaec9c05 | ||
|
|
43a0d1778f | ||
|
|
d514ebdd7e | ||
|
|
8480a85ce2 | ||
|
|
ad98d38efb | ||
|
|
1358425e1a | ||
|
|
43fa0d7425 | ||
|
|
86eb495641 | ||
|
|
5485f0b9cd | ||
|
|
4974403661 | ||
|
|
deb631b456 | ||
|
|
238c489f3a | ||
|
|
3c9fa7ba73 | ||
|
|
e8e8252c52 | ||
|
|
50f1e69268 | ||
|
|
0dcd5fa371 | ||
|
|
75cbb11d00 | ||
|
|
020473afc4 | ||
|
|
497641a00d | ||
|
|
bc860c733a | ||
|
|
d774a8428a | ||
|
|
a593269f61 | ||
|
|
89fd52a602 | ||
|
|
7f061d2362 | ||
|
|
2c1e1532ad | ||
|
|
0c52b50e65 | ||
|
|
6db70f4143 | ||
|
|
1800530bf2 | ||
|
|
32a5ee91df | ||
|
|
5c0315d3bb | ||
|
|
12f4ec404c | ||
|
|
8aa39c117f | ||
|
|
017216533b | ||
|
|
499bda4533 | ||
|
|
9ef564f0be | ||
|
|
d303527c3d | ||
|
|
51a8f2fc34 | ||
|
|
c56dc98d13 | ||
|
|
454f6a5d63 | ||
|
|
87408e529c | ||
|
|
24bec8ce81 | ||
|
|
d9b47bc864 | ||
|
|
6912fa14f2 | ||
|
|
45291f71f4 | ||
|
|
409192d063 | ||
|
|
309fe4c964 | ||
|
|
34891f5657 | ||
|
|
43df77f6b6 | ||
|
|
78c6a0b991 | ||
|
|
d3e91222c8 | ||
|
|
f2dadd8f8f | ||
|
|
ffedb6e40e | ||
|
|
fdcd01ba35 | ||
|
|
bd3ecc3fb1 | ||
|
|
dbc0bf69e8 | ||
|
|
23b1096caa | ||
|
|
8805792089 | ||
|
|
ef9cbe6bcc | ||
|
|
fc19705ecd | ||
|
|
d4654138ee | ||
|
|
930a45dafe | ||
|
|
f1a8ce0bba | ||
|
|
287974a93a | ||
|
|
4c081c6723 | ||
|
|
58ae10f887 | ||
|
|
f673efcba5 | ||
|
|
503da432dc | ||
|
|
4fb7eb8643 | ||
|
|
ae303c73de | ||
|
|
ad11fbf97b | ||
|
|
9ed6711a7e | ||
|
|
fa0e853bd0 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["@appium/eslint-config-appium", "@appium/eslint-config-appium-ts"],
|
||||
"extends": ["@appium/eslint-config-appium-ts"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "packages/fake-driver/**/*",
|
||||
@@ -26,9 +26,16 @@
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["./test/setup.js", "./**/scripts/**/*.js", "./packages/*/index.js"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "./packages/*/test/**/*.js",
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
|
||||
+1
-1
@@ -53,4 +53,4 @@ targets:
|
||||
path: ./packages/docutils
|
||||
|
||||
revision:
|
||||
- branch: master
|
||||
branch: master
|
||||
|
||||
+12
-1
@@ -25,6 +25,7 @@ labels:
|
||||
matcher:
|
||||
commits: '^chore.*?:'
|
||||
title: '^chore.*?:'
|
||||
files: ['.github/**', '.husky/**', '.vscode/**', './.*', './*.js', '**/test/**']
|
||||
|
||||
- label: 'SemVer:Breaking'
|
||||
sync: true
|
||||
@@ -34,7 +35,7 @@ labels:
|
||||
- label: 'Dependencies'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['**/package.json', '**/package-lock.json', '**/requirements.txt']
|
||||
files: ['**/package.json', 'package-lock.json', '**/requirements.txt']
|
||||
author: 'renovate'
|
||||
|
||||
- label: 'appium core'
|
||||
@@ -52,6 +53,11 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/eslint-config-appium/**']
|
||||
|
||||
- label: '@appium/eslint-config-appium-ts'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/eslint-config-appium-ts/**']
|
||||
|
||||
- label: '@appium/base-plugin'
|
||||
sync: true
|
||||
matcher:
|
||||
@@ -141,3 +147,8 @@ labels:
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/universal-xml-plugin/**']
|
||||
|
||||
- label: '@appium/strongbox'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/strongbox/**']
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Labeler
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
branches: [master]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
name: Auto-Label PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fuxingloh/multi-labeler@6704db0bcba106d07482efabbc79d3092af74fa2 # v2
|
||||
+41
-22
@@ -1,5 +1,42 @@
|
||||
name: Appium Build
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
# this list of paths could potentially impact the build
|
||||
paths:
|
||||
- '**/tsconfig*.json'
|
||||
- 'packages/base-driver/static/**/*'
|
||||
- 'packages/*/test/**'
|
||||
- 'packages/*/bin'
|
||||
- 'packages/**/*.ts'
|
||||
- 'packages/**/*.json'
|
||||
- 'packages/**/*.js'
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/tsconfig*.json'
|
||||
- 'packages/base-driver/static/**/*'
|
||||
- 'packages/*/test/**'
|
||||
- 'packages/*/bin'
|
||||
- 'packages/**/*.ts'
|
||||
- 'packages/**/*.json'
|
||||
- 'packages/**/*.js'
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,7 +50,7 @@ jobs:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
@@ -33,33 +70,15 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
useRollingCache: true
|
||||
install-command: npm ci
|
||||
- name: ESLint
|
||||
run: npm run lint
|
||||
run: npm run lint:ci
|
||||
- name: Validate Renovate Config
|
||||
uses: rinchsan/renovate-config-validator@1ea1e8514f6a33fdd71c40b0a5fa3512b9e7b936 # tag=v0
|
||||
with:
|
||||
pattern: '{.renovaterc.json,renovate/default.json}'
|
||||
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- name: Install dependencies (node)
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
useRollingCache: true
|
||||
install-command: npm ci
|
||||
- name: Install dependencies (python)
|
||||
run: pip install -r packages/docutils/requirements.txt
|
||||
- name: Build Docs
|
||||
run: |
|
||||
git config user.name 'Appium Bot'
|
||||
git config user.email 'appium@appium.io'
|
||||
npm run --workspace packages/appium build:docs
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Build Docs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '**/typedoc.json'
|
||||
- '**/tsconfig*.json'
|
||||
- '**/package*.json'
|
||||
- '**/*mkdocs*.ya?ml'
|
||||
- 'packages/docutils/requirements.txt'
|
||||
- 'packages/*/README.md'
|
||||
- 'packages/*/docs/**'
|
||||
- 'packages/*/**/*.ts'
|
||||
- 'packages/*/**/*.js'
|
||||
- '.github/workflows/docs.yml' # this file
|
||||
- '!**/test/**'
|
||||
- '!**/sample-code/**'
|
||||
|
||||
branches: [master]
|
||||
|
||||
concurrency: preview-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- name: Install dependencies (Node.js)
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
useRollingCache: true
|
||||
install-command: npm ci
|
||||
- name: Install dependencies (Python)
|
||||
run: pip install -r packages/docutils/requirements.txt
|
||||
- name: Configure Git User
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
- name: Build Docs
|
||||
run: npm run docs:preview
|
||||
- name: Deploy Preview
|
||||
uses: rossjrw/pr-preview-action@2a652922e9b9c53e7e5ea62fa38da744de09043c # v1
|
||||
with:
|
||||
source-dir: ./packages/appium/docs/site
|
||||
custom-url: 'appium.github.io/appium'
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Run FOSSA Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
with:
|
||||
ref: master
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: 'Labeler'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
# Setting up permissions in the workflow to limit the scope of what it can do. Optional!
|
||||
contents: read
|
||||
issues: read # change this if/when we want to label issues
|
||||
pull-requests: write
|
||||
statuses: read
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
name: Labeler
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# follows semantic versioning. Lock to different version: v1, v1.5, v1.5.0 or use a commit hash.
|
||||
- uses: fuxingloh/multi-labeler@6704db0bcba106d07482efabbc79d3092af74fa2 # v2
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}'
|
||||
@@ -1,15 +1,15 @@
|
||||
name: Disallow "On Hold" PRs
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
label-check:
|
||||
if: github.base_ref == 'master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danielchabr/pr-labels-checker@e1d7e79aa0933af1728011705a30528a279874fd
|
||||
- name: Check for "On Hold" label
|
||||
uses: egmacke/action-check-label@0eb29f801fb482a95c9bd2dd22834ecc8f4e8af9 # v2
|
||||
with:
|
||||
id: check-labels
|
||||
hasNone: On Hold
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
label: On Hold
|
||||
state: absent
|
||||
|
||||
@@ -59,7 +59,6 @@ appium.zip
|
||||
*.tsbuildinfo
|
||||
packages/*/LICENSE
|
||||
local_appium_home
|
||||
jsdoc-out
|
||||
*.tgz
|
||||
typedoc-docs
|
||||
packages/appium/docs/*/reference
|
||||
|
||||
+5
-5
@@ -17,9 +17,9 @@ module.exports = (wallaby) => {
|
||||
},
|
||||
files: [
|
||||
'./packages/*/build/**/*',
|
||||
'./packages/*/lib/**/*.(j|t)s',
|
||||
'./packages/*/test/**/*helper*.(j|t)s',
|
||||
'./packages/*/test/**/*mock*.(j|t)s',
|
||||
'./packages/*/lib/**/*.{js,ts}',
|
||||
'./packages/*/test/**/*helper*.{js,ts}',
|
||||
'./packages/*/test/**/*mock*.{js,ts}',
|
||||
'./packages/*/package.json',
|
||||
'./packages/*/tsconfig*.json',
|
||||
'./packages/*/test/**/fixture?(s)/**/*',
|
||||
@@ -44,11 +44,11 @@ module.exports = (wallaby) => {
|
||||
pattern: './packages/typedoc-plugin-appium/lib/theme/resources/**',
|
||||
},
|
||||
'!./packages/*/test/**/*-specs.js',
|
||||
'!./packages/*/test/**/*.e2e.spec.(j|t)s',
|
||||
'!./packages/*/test/**/*.e2e.spec.{js,ts}',
|
||||
'!**/local_appium_home/**',
|
||||
],
|
||||
testFramework: 'mocha',
|
||||
tests: ['./packages/*/test/unit/**/*.spec.(j|t)s', '!**/local_appium_home/**'],
|
||||
tests: ['./packages/*/test/unit/**/*.spec.{js,ts}', '!**/local_appium_home/**'],
|
||||
workers: {
|
||||
// restart: true,
|
||||
},
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
Appium is an open-source, cross-platform test automation tool for native,
|
||||
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
|
||||
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
|
||||
See [Drivers Maintained By The Appium Team](#drivers-maintained-by-the-appium-team)
|
||||
and [Drivers Provided By Third Parties](#drivers-provided-by-third-parties) sections below for more details.
|
||||
See the full list of
|
||||
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
|
||||
|
||||
:bangbang: Major documentation revision in progress
|
||||
|
||||
|
||||
+10
-5
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/lerna",
|
||||
"changelogPreset": "conventional-changelog-conventionalcommits",
|
||||
"command": {
|
||||
"add": {
|
||||
"exact": true
|
||||
},
|
||||
"publish": {
|
||||
"message": "chore: publish"
|
||||
},
|
||||
"run": {
|
||||
"concurrency": 8
|
||||
},
|
||||
@@ -9,13 +17,10 @@
|
||||
"version": {
|
||||
"allowBranch": "master"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
"exact": true
|
||||
}
|
||||
},
|
||||
"ignoreChanges": ["**/test/**", "**/*.md"],
|
||||
"version": "independent",
|
||||
"useNx": false,
|
||||
"useWorkspaces": true,
|
||||
"useNx": false
|
||||
"version": "independent"
|
||||
}
|
||||
|
||||
Generated
+4299
-846
File diff suppressed because it is too large
Load Diff
+47
-33
@@ -27,43 +27,54 @@
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"build:all": "run-s build build:packages",
|
||||
"build:packages": "lerna run build",
|
||||
"clean": "run-s clean:artifacts clean:workspaces",
|
||||
"clean:artifacts": "run-p \"clean:artifacts:*\"",
|
||||
"clean:artifacts:distfiles": "npx rimraf \"./packages/*/build\" \"./packages/*/.tsconfig.tsbuildinfo\"",
|
||||
"clean:artifacts:packages": "lerna run clean || true",
|
||||
"clean:workspaces": "lerna clean -y",
|
||||
"build": "run-s build:*",
|
||||
"build:compile": "tsc -b",
|
||||
"build:workspaces": "lerna run build",
|
||||
"ci": "npm ci",
|
||||
"dev": "run-s \"build -- --watch\"",
|
||||
"clean": "run-s clean:distfiles clean:workspaces",
|
||||
"clean:dangerously": "run-s clean:workspaces clean:force",
|
||||
"clean:deps": "lerna clean --yes",
|
||||
"clean:distfiles": "npx rimraf \"./packages/*/build\" \"./packages/*/**/.tsconfig.tsbuildinfo\"; npx nx reset",
|
||||
"clean:interactive": "git clean -d -x -i",
|
||||
"clean:force": "git clean -d -x -f",
|
||||
"clean:workspaces": "lerna run clean || true",
|
||||
"dev": "run-s build:compile \"build:workspaces\" \"build:compile -- --watch\"",
|
||||
"dev:docs": "npm run -w packages/appium dev:docs",
|
||||
"docs": "npm run -w packages/appium build:docs",
|
||||
"docs:preview": "npm run -w packages/appium build:docs:preview",
|
||||
"doctor": "node ./packages/doctor",
|
||||
"husky-install": "husky install",
|
||||
"generate-docs": "npm run --workspace=packages/appium generate-docs",
|
||||
"prepare": "run-p husky-install build:all",
|
||||
"husky-install": "husky install",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "run-s \"lint -- --fix\"",
|
||||
"lint:ci": "eslint --quiet .",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:fix": "run-s \"lint -- --fix\"",
|
||||
"lint:staged": "lint-staged",
|
||||
"prepare": "run-p husky-install rebuild",
|
||||
"prepublishOnly": "run-s rebuild",
|
||||
"reinstall": "run-s clean ci",
|
||||
"rebuild": "run-s clean:artifacts build:all",
|
||||
"preversion": "run-s rebuild",
|
||||
"version": "run-s sync-pkgs stage-synced-pkgs",
|
||||
"rebuild": "run-s clean build",
|
||||
"reinstall": "run-s clean clean:deps ci",
|
||||
"reinstall:angry": "npm run clean:dangerously && npm ci",
|
||||
"release": "lerna publish",
|
||||
"stage-synced-pkgs": "git add -A packages/appium/README.md \"packages/*/package.json\"",
|
||||
"start": "appium",
|
||||
"sync-pkgs": "run-p sync-pkgs:*",
|
||||
"sync-pkgs:appium-readme": "sync-monorepo-packages --force --no-package-json --packages=packages/appium README.md",
|
||||
"sync-pkgs:common-fields": "sync-monorepo-packages --fields=author,license,bugs,homepage,engines",
|
||||
"sync-pkgs:keywords": "sync-monorepo-packages --field=keywords --packages=\"packages/*\" --packages=\"!packages/eslint-config-appium\" --packages=\"!packages/types\" --packages=\"!packages/typedoc-plugin-appium\" --packages=\"!packages/eslint-config-appium-ts\" ",
|
||||
"sync-pkgs:license": "sync-monorepo-packages --force --no-package-json LICENSE",
|
||||
"stage-synced-pkgs": "git add -A packages/appium/README.md \"packages/*/package.json\"",
|
||||
"test": "npm-run-all -p lint build -s test:unit",
|
||||
"test:ci": "run-s test:smoke test:unit test:e2e",
|
||||
"test": "run-s test:quick",
|
||||
"test:ci": "run-s test:smoke test:unit test:types test:e2e",
|
||||
"test:e2e": "lerna run --concurrency=2 test:e2e",
|
||||
"test:quick": "run-s lint test:unit test:types",
|
||||
"test:slow": "run-s test:quick test:smoke test:e2e",
|
||||
"test:smoke": "smoker --all test:smoke",
|
||||
"test:types": "lerna run test:types",
|
||||
"test:unit": "lerna run test",
|
||||
"typedoc": "typedoc --logLevel Verbose",
|
||||
"upload": "gulp github-upload",
|
||||
"preversion": "run-s sync-pkgs stage-synced-pkgs",
|
||||
"prepack": "npm run rebuild",
|
||||
"zip": "zip -qr ./appium.zip ./packages/appium",
|
||||
"zip-and-upload": "run-s zip upload"
|
||||
},
|
||||
@@ -74,30 +85,32 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"@commitlint/cli": "17.4.4",
|
||||
"@commitlint/cli": "17.5.1",
|
||||
"@commitlint/config-conventional": "17.4.4",
|
||||
"@tsconfig/node14": "1.0.3",
|
||||
"@types/chai": "4.3.4",
|
||||
"@types/chai-as-promised": "7.1.5",
|
||||
"@types/diff": "5.0.2",
|
||||
"@types/diff": "5.0.3",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/node": "18.14.6",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/sinon-chai": "3.2.9",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"@types/ws": "8.5.4",
|
||||
"@types/yargs": "17.0.22",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"@types/yargs": "17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.58.0",
|
||||
"asyncbox": "2.9.4",
|
||||
"chai": "4.3.7",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"chai-webdriverio-async": "2.7.0",
|
||||
"conventional-changelog-conventionalcommits": "5.0.0",
|
||||
"cpy-cli": "4.2.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-prettier": "8.7.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-find-rules": "4.1.0",
|
||||
"eslint-import-resolver-typescript": "3.5.5",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-mocha": "10.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
@@ -107,30 +120,31 @@
|
||||
"handlebars": "4.7.7",
|
||||
"husky": "8.0.3",
|
||||
"json-schema-to-typescript": "12.0.0",
|
||||
"lerna": "6.5.1",
|
||||
"lint-staged": "13.1.2",
|
||||
"lerna": "6.6.1",
|
||||
"lint-staged": "13.2.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"midnight-smoker": "2.0.1",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
"mjpeg-server": "0.3.1",
|
||||
"mocha": "10.2.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.8.4",
|
||||
"prettier": "2.8.7",
|
||||
"rewiremock": "3.14.5",
|
||||
"rimraf": "3.0.2",
|
||||
"serve-static": "1.15.0",
|
||||
"sinon": "15.0.1",
|
||||
"sinon": "15.0.3",
|
||||
"sinon-chai": "3.7.0",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"through2": "4.0.2",
|
||||
"ts-node": "10.9.1",
|
||||
"typedoc": "0.23.26",
|
||||
"tsd": "0.28.1",
|
||||
"typedoc": "0.23.28",
|
||||
"typedoc-plugin-markdown": "3.14.0",
|
||||
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
|
||||
"typescript": "4.9.5",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "7.30.2",
|
||||
"ws": "8.12.1",
|
||||
"ws": "8.13.0",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,79 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.0-beta.63](https://github.com/appium/appium/compare/appium@2.0.0-beta.62...appium@2.0.0-beta.63) (2023-04-12)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.62](https://github.com/appium/appium/compare/appium@2.0.0-beta.61...appium@2.0.0-beta.62) (2023-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
|
||||
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** make "ls" alias of "list" ([5ab2fea](https://github.com/appium/appium/commit/5ab2fea5768ef8c67270d49c920b9515ca1ff1e4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.61](https://github.com/appium/appium/compare/appium@2.0.0-beta.60...appium@2.0.0-beta.61) (2023-04-05)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.60](https://github.com/appium/appium/compare/appium@2.0.0-beta.59...appium@2.0.0-beta.60) (2023-04-04)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.59](https://github.com/appium/appium/compare/appium@2.0.0-beta.58...appium@2.0.0-beta.59) (2023-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** update dependency package-changed to v3 ([bbcaec9](https://github.com/appium/appium/commit/bbcaec9c05e93d522ae72e30368cecc876e16db7))
|
||||
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.58](https://github.com/appium/appium/compare/appium@2.0.0-beta.57...appium@2.0.0-beta.58) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
|
||||
* **appium:** remove longjohn ([0c52b50](https://github.com/appium/appium/commit/0c52b50e6595a16287d0bb71960fb2e08278de6f)), closes [#18357](https://github.com/appium/appium/issues/18357)
|
||||
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
|
||||
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
|
||||
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** improvements to ext commands when running extension in development ([d9b47bc](https://github.com/appium/appium/commit/d9b47bc86434c50382221397cf39c10e661c1ea9)), closes [#18277](https://github.com/appium/appium/issues/18277)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.57](https://github.com/appium/appium/compare/appium@2.0.0-beta.56...appium@2.0.0-beta.57) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
Appium is an open-source, cross-platform test automation tool for native,
|
||||
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
|
||||
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
|
||||
See [Drivers Maintained By The Appium Team](#drivers-maintained-by-the-appium-team)
|
||||
and [Drivers Provided By Third Parties](#drivers-provided-by-third-parties) sections below for more details.
|
||||
See the full list of
|
||||
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
|
||||
|
||||
:bangbang: Major documentation revision in progress
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ These drivers are not maintained by the Appium team and can be used to target ad
|
||||
|
||||
## Clients
|
||||
|
||||
You need a clients to write and run Appium scripts. To learn more about clients, read our [Client
|
||||
You need clients to write and run Appium scripts. To learn more about clients, read our [Client
|
||||
Intro](../intro/clients.md). Here is the list of known Appium clients. You'll want to become very
|
||||
familiar with your client documentation (as well as the documentation of any Selenium client that
|
||||
the Appium client depends on) since that is what you will use as your primary interface to Appium.
|
||||
|
||||
@@ -20,7 +20,7 @@ JavaScript, Python, and Java, though [Appium clients](../ecosystem/index.md#clie
|
||||
Before getting started, make sure your system satisfies the
|
||||
[requirements](../intro/requirements.md) for running the Appium server. Additional requirements
|
||||
will be discussed in conjunction with installing the UiAutomator2 driver. The guide also assumes
|
||||
you're basic command line proficiency on your platform, for example being able to run commands, set
|
||||
you have basic command line proficiency on your platform, for example being able to run commands, set
|
||||
and persist environment variables, etc...
|
||||
|
||||
Now you're ready to get started! So head on over to [Installing Appium](./install.md).
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||
/* eslint-disable promise/prefer-await-to-then */
|
||||
|
||||
const {buildReferenceDocs, deploy, updateNav} = require('@appium/docutils');
|
||||
const {buildReferenceDocs, deploy, updateNav, buildSite} = require('@appium/docutils');
|
||||
const {
|
||||
log,
|
||||
LANGS,
|
||||
@@ -28,6 +28,7 @@ const {version} = require('../../package.json');
|
||||
const branch = process.env.APPIUM_DOCS_BRANCH ?? DOCS_BRANCH;
|
||||
const basePrefix = process.env.APPIUM_DOCS_PREFIX ?? DOCS_PREFIX;
|
||||
const remote = process.env.APPIUM_DOCS_REMOTE ?? DOCS_REMOTE;
|
||||
const preview = Boolean(process.env.APPIUM_DOCS_PREVIEW);
|
||||
|
||||
const push = Boolean(process.env.APPIUM_DOCS_PUBLISH);
|
||||
const rebase = push;
|
||||
@@ -51,17 +52,24 @@ async function main() {
|
||||
for (const lang of LANGS) {
|
||||
log.info(`Building docs for language '${lang}' and version ${majMinVer}`);
|
||||
const configFile = path.join(DOCS_DIR, `mkdocs-${lang}.yml`);
|
||||
await deploy({
|
||||
mkdocsYml: configFile,
|
||||
branch,
|
||||
prefix: path.join(basePrefix, lang),
|
||||
remote,
|
||||
deployVersion: majMinVer,
|
||||
push,
|
||||
alias: LATEST_ALIAS,
|
||||
rebase,
|
||||
message: `docs(appium): auto-build docs for appium@${majMinVer}, language ${lang}`,
|
||||
});
|
||||
if (preview) {
|
||||
await buildSite({
|
||||
mkdocsYml: configFile,
|
||||
siteDir: path.join('site', lang),
|
||||
});
|
||||
} else {
|
||||
await deploy({
|
||||
mkdocsYml: configFile,
|
||||
branch,
|
||||
prefix: path.join(basePrefix, lang),
|
||||
remote,
|
||||
deployVersion: majMinVer,
|
||||
push,
|
||||
alias: LATEST_ALIAS,
|
||||
rebase,
|
||||
message: `docs(appium): auto-build docs for appium@${majMinVer}, language ${lang}`,
|
||||
});
|
||||
}
|
||||
log.info(`Docs built`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
const {baseConfig} = require('@appium/docutils');
|
||||
|
||||
const lang = process.env.APPIUM_DOCS_LANG || 'en';
|
||||
|
||||
const config = {...baseConfig};
|
||||
config.opts.source.include = [];
|
||||
config.opts.tutorials = `./docs/${lang}`;
|
||||
config.opts.readme = `${config.opts.tutorials}/README.md`;
|
||||
|
||||
module.exports = config;
|
||||
@@ -28,15 +28,12 @@ const desiredCapabilityConstraints = /** @type {const} */ ({
|
||||
isString: true,
|
||||
},
|
||||
});
|
||||
/**
|
||||
* @typedef {typeof desiredCapabilityConstraints} AppiumDriverConstraints
|
||||
*/
|
||||
|
||||
const sessionsListGuard = new AsyncLock();
|
||||
const pendingDriversGuard = new AsyncLock();
|
||||
|
||||
/**
|
||||
* @implements {SessionHandler}
|
||||
* @extends {DriverCore<AppiumDriverConstraints>}
|
||||
*/
|
||||
class AppiumDriver extends DriverCore {
|
||||
/**
|
||||
@@ -92,11 +89,11 @@ class AppiumDriver extends DriverCore {
|
||||
*/
|
||||
desiredCapConstraints;
|
||||
|
||||
/** @type {DriverOpts} */
|
||||
/** @type {import('@appium/types').DriverOpts<AppiumDriverConstraints>} */
|
||||
args;
|
||||
|
||||
/**
|
||||
* @param {DriverOpts} opts
|
||||
* @param {import('@appium/types').DriverOpts<AppiumDriverConstraints>} opts
|
||||
*/
|
||||
constructor(opts) {
|
||||
// It is necessary to set `--tmp` here since it should be set to
|
||||
@@ -163,11 +160,10 @@ class AppiumDriver extends DriverCore {
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async getSessions() {
|
||||
return _.toPairs(this.sessions).map(([id, driver]) => ({
|
||||
id,
|
||||
capabilities: driver.caps,
|
||||
capabilities: /** @type {import('@appium/types').DriverCaps} */ (driver.caps),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -224,12 +220,12 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
* @param {W3CCapabilities<AppiumDriverConstraints>} jsonwpCaps JSONWP formatted desired capabilities
|
||||
* @param {W3CCapabilities<AppiumDriverConstraints>} reqCaps Required capabilities (JSONWP standard)
|
||||
* @param {W3CCapabilities<AppiumDriverConstraints>} w3cCapabilities W3C capabilities
|
||||
* @param {DriverData[]} [driverData]
|
||||
* @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
|
||||
* @param {W3CAppiumDriverCaps} [reqCaps] Required capabilities (JSONWP standard)
|
||||
* @param {W3CAppiumDriverCaps} [w3cCapabilities] W3C capabilities
|
||||
* @returns {Promise<SessionHandlerCreateResult>}
|
||||
*/
|
||||
async createSession(jsonwpCaps, reqCaps, w3cCapabilities, driverData) {
|
||||
async createSession(jsonwpCaps, reqCaps, w3cCapabilities) {
|
||||
const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
|
||||
const defaultSettings = pullSettings(defaultCapabilities);
|
||||
jsonwpCaps = _.cloneDeep(jsonwpCaps);
|
||||
@@ -247,13 +243,14 @@ class AppiumDriver extends DriverCore {
|
||||
Object.assign(w3cSettings, pullSettings(firstMatchEntry));
|
||||
}
|
||||
|
||||
/** @type {string|undefined} */
|
||||
let protocol;
|
||||
let innerSessionId, dCaps;
|
||||
try {
|
||||
// Parse the caps into a format that the InnerDriver will accept
|
||||
const parsedCaps = parseCapsForInnerDriver(
|
||||
jsonwpCaps,
|
||||
promoteAppiumOptions(w3cCapabilities),
|
||||
promoteAppiumOptions(/** @type {W3CAppiumDriverCaps} */ (w3cCapabilities)),
|
||||
this.desiredCapConstraints,
|
||||
defaultCapabilities ? promoteAppiumOptionsForObject(defaultCapabilities) : undefined
|
||||
);
|
||||
@@ -399,7 +396,7 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Driver} driver
|
||||
* @param {ExternalDriver} driver
|
||||
* @param {string} innerSessionId
|
||||
*/
|
||||
attachUnexpectedShutdownHandler(driver, innerSessionId) {
|
||||
@@ -442,10 +439,11 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DriverClass} InnerDriver
|
||||
* @param {((...args: any[]) => any)|(new(...args: any[]) => any)} InnerDriver
|
||||
* @returns {Promise<DriverData[]>}}
|
||||
* @privateRemarks The _intent_ is that `InnerDriver` is the class of a driver, but it only really
|
||||
* needs to be a function or constructor.
|
||||
*/
|
||||
// eslint-disable-next-line require-await
|
||||
async curSessionDataForDriver(InnerDriver) {
|
||||
const data = _.compact(
|
||||
_.values(this.sessions)
|
||||
@@ -588,7 +586,7 @@ class AppiumDriver extends DriverCore {
|
||||
*
|
||||
* @param {string} cmd
|
||||
* @param {...any} args
|
||||
* @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<Driver['executeCommand']>>}
|
||||
* @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<ExternalDriver['executeCommand']>>}
|
||||
*/
|
||||
async executeCommand(cmd, ...args) {
|
||||
// We have basically three cases for how to handle commands:
|
||||
@@ -618,6 +616,7 @@ class AppiumDriver extends DriverCore {
|
||||
let dstSession = null;
|
||||
let protocol = null;
|
||||
/** @type {this | ExternalDriver} */
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
let driver = this;
|
||||
if (isSessionCmd) {
|
||||
sessionId = _.last(args);
|
||||
@@ -840,21 +839,38 @@ export class NoDriverProxyCommandError extends Error {
|
||||
export {AppiumDriver};
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
||||
* @typedef {import('@appium/types').Driver} Driver
|
||||
* @typedef {import('@appium/types').DriverClass} DriverClass
|
||||
* @typedef {import('@appium/types').DriverData} DriverData
|
||||
* @typedef {import('@appium/types').ServerArgs} DriverOpts
|
||||
* @typedef {import('@appium/types').Constraints} Constraints
|
||||
* @typedef {import('@appium/types').AppiumServer} AppiumServer
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('./extension/driver-config').DriverConfig} DriverConfig
|
||||
* @typedef {import('@appium/types').Plugin} Plugin
|
||||
* @typedef {import('@appium/types').PluginClass} PluginClass
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').StringRecord} StringRecord
|
||||
* @typedef {import('@appium/types').SessionHandler<SessionHandlerResult<any[]>,SessionHandlerResult<void>>} SessionHandler
|
||||
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
||||
* @typedef {import('@appium/types').PluginClass} PluginClass
|
||||
* @typedef {import('@appium/types').Plugin} Plugin
|
||||
* @typedef {import('@appium/types').DriverClass<import('@appium/types').Driver>} DriverClass
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ISessionHandler<AppiumDriverConstraints,
|
||||
* SessionHandlerCreateResult, SessionHandlerDeleteResult>} AppiumSessionHandler
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SessionHandlerResult<[innerSessionId: string, caps:
|
||||
* import('@appium/types').DriverCaps<Constraints>, protocol: string|undefined]>} SessionHandlerCreateResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Core<C>} Core
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SessionHandlerResult<void>} SessionHandlerDeleteResult
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -868,11 +884,6 @@ export {AppiumDriver};
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
|
||||
* @typedef {typeof desiredCapabilityConstraints} AppiumDriverConstraints
|
||||
* @typedef {import('@appium/types').W3CDriverCaps<AppiumDriverConstraints>} W3CAppiumDriverCaps
|
||||
*/
|
||||
|
||||
@@ -84,6 +84,16 @@ function makeListArgs(type) {
|
||||
dest: 'showUpdates',
|
||||
},
|
||||
],
|
||||
[
|
||||
['--verbose'],
|
||||
{
|
||||
required: false,
|
||||
default: false,
|
||||
action: 'store_true',
|
||||
help: 'Show more information about each extension',
|
||||
dest: 'verbose',
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -284,5 +294,5 @@ export {getServerArgs, getExtensionArgs};
|
||||
|
||||
/**
|
||||
* A tuple of argument aliases and argument options
|
||||
* @typedef {Map<string[],import('argparse').ArgumentOptions>} ArgumentDefinitions
|
||||
* @typedef {Map<[name: string]|[name: string, alias: string],import('argparse').ArgumentOptions>} ArgumentDefinitions
|
||||
*/
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import ExtensionCommand from './extension-command';
|
||||
import ExtensionCliCommand from './extension-command';
|
||||
import {KNOWN_DRIVERS} from '../constants';
|
||||
import '@colors/colors';
|
||||
|
||||
const REQ_DRIVER_FIELDS = ['driverName', 'automationName', 'platformNames', 'mainClass'];
|
||||
|
||||
/**
|
||||
* @extends {ExtensionCommand<DriverType>}
|
||||
* @extends {ExtensionCliCommand<DriverType>}
|
||||
*/
|
||||
|
||||
export default class DriverCommand extends ExtensionCommand {
|
||||
export default class DriverCliCommand extends ExtensionCliCommand {
|
||||
/**
|
||||
* @param {import('./extension-command').ExtensionCommandOptions<DriverType>} opts
|
||||
*/
|
||||
@@ -120,7 +120,7 @@ export default class DriverCommand extends ExtensionCommand {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode DriverCommand.install}
|
||||
* Options for {@linkcode DriverCliCommand.install}
|
||||
* @typedef DriverInstallOpts
|
||||
* @property {string} driver - the name or spec of a driver to install
|
||||
* @property {InstallType} installType - how to install this driver. One of the INSTALL_TYPES
|
||||
@@ -132,20 +132,20 @@ export default class DriverCommand extends ExtensionCommand {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode DriverCommand.uninstall}
|
||||
* Options for {@linkcode DriverCliCommand.uninstall}
|
||||
* @typedef DriverUninstallOpts
|
||||
* @property {string} driver - the name or spec of a driver to uninstall
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode DriverCommand.update}
|
||||
* Options for {@linkcode DriverCliCommand.update}
|
||||
* @typedef DriverUpdateOpts
|
||||
* @property {string} driver - the name of the driver to update
|
||||
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode DriverCommand.run}.
|
||||
* Options for {@linkcode DriverCliCommand.run}.
|
||||
* @typedef DriverRunOptions
|
||||
* @property {string} driver - name of the driver to run a script from
|
||||
* @property {string} scriptName - name of the script to run
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
INSTALL_TYPE_GIT,
|
||||
INSTALL_TYPE_GITHUB,
|
||||
INSTALL_TYPE_LOCAL,
|
||||
INSTALL_TYPE_DEV,
|
||||
} from '../extension/extension-config';
|
||||
import {SubProcess} from 'teen_process';
|
||||
import {packageDidChange} from '../extension/package-changed';
|
||||
import {spawn} from 'child_process';
|
||||
import {inspect} from 'node:util';
|
||||
|
||||
const UPDATE_ALL = 'installed';
|
||||
|
||||
@@ -32,7 +34,7 @@ function receiptToManifest(receipt) {
|
||||
/**
|
||||
* @template {ExtensionType} ExtType
|
||||
*/
|
||||
class ExtensionCommand {
|
||||
class ExtensionCliCommand {
|
||||
/**
|
||||
* This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
|
||||
* @type {ExtensionConfig<ExtType>}
|
||||
@@ -99,41 +101,40 @@ class ExtensionCommand {
|
||||
|
||||
/**
|
||||
* List extensions
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ListOptions} opts
|
||||
* @return {Promise<ExtensionListData>} map of extension names to extension data
|
||||
* @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
|
||||
*/
|
||||
async list({showInstalled, showUpdates}) {
|
||||
const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
|
||||
async list({showInstalled, showUpdates, verbose = false}) {
|
||||
let lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
|
||||
if (verbose) {
|
||||
lsMsg += ' (verbose mode)';
|
||||
}
|
||||
const installedNames = Object.keys(this.config.installedExtensions);
|
||||
const knownNames = Object.keys(this.knownExtensions);
|
||||
const exts = [...installedNames, ...knownNames].reduce(
|
||||
(acc, name) => {
|
||||
if (!acc[name]) {
|
||||
if (installedNames.includes(name)) {
|
||||
acc[name] = {
|
||||
...this.config.installedExtensions[name],
|
||||
installed: true,
|
||||
};
|
||||
} else if (!showInstalled) {
|
||||
acc[name] = {pkgName: this.knownExtensions[name], installed: false};
|
||||
}
|
||||
const listData = [...installedNames, ...knownNames].reduce((acc, name) => {
|
||||
if (!acc[name]) {
|
||||
if (installedNames.includes(name)) {
|
||||
acc[name] = {
|
||||
.../** @type {Partial<ExtManifest<ExtType>>} */ (this.config.installedExtensions[name]),
|
||||
installed: true,
|
||||
};
|
||||
} else if (!showInstalled) {
|
||||
acc[name] = /** @type {ExtensionListData<ExtType>} */ ({
|
||||
pkgName: this.knownExtensions[name],
|
||||
installed: false,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
/**
|
||||
* This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
|
||||
* {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
|
||||
* @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
|
||||
*/ ({})
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, /** @type {ExtensionList<ExtType>} */ ({}));
|
||||
|
||||
// if we want to show whether updates are available, put that behind a spinner
|
||||
await spinWith(this.isJsonOutput, lsMsg, async () => {
|
||||
if (!showUpdates) {
|
||||
return;
|
||||
}
|
||||
for (const [ext, data] of _.toPairs(exts)) {
|
||||
for (const [ext, data] of _.toPairs(listData)) {
|
||||
if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
|
||||
// don't need to check for updates on exts that aren't installed
|
||||
// also don't need to check for updates on non-npm exts
|
||||
@@ -150,7 +151,12 @@ class ExtensionCommand {
|
||||
}
|
||||
});
|
||||
|
||||
const listData = /** @type {ExtensionListData} */ (exts);
|
||||
/**
|
||||
* Type guard to narrow "installed" extensions, which have more data
|
||||
* @param {any} data
|
||||
* @returns {data is InstalledExtensionListData<ExtType>}
|
||||
*/
|
||||
const extIsInstalled = (data) => Boolean(data.installed);
|
||||
|
||||
// if we're just getting the data, short circuit return here since we don't need to do any
|
||||
// formatting logic
|
||||
@@ -158,12 +164,16 @@ class ExtensionCommand {
|
||||
return listData;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
this.log.log(inspect(listData, {colors: true, depth: null}));
|
||||
return listData;
|
||||
}
|
||||
for (const [name, data] of _.toPairs(listData)) {
|
||||
let installTxt = ' [not installed]'.grey;
|
||||
let updateTxt = '';
|
||||
let upToDateTxt = '';
|
||||
let unsafeUpdateTxt = '';
|
||||
if (data.installed) {
|
||||
if (extIsInstalled(data)) {
|
||||
const {
|
||||
installType,
|
||||
installSpec,
|
||||
@@ -182,8 +192,11 @@ class ExtensionCommand {
|
||||
case INSTALL_TYPE_LOCAL:
|
||||
typeTxt = `(linked from ${installSpec})`.magenta;
|
||||
break;
|
||||
case INSTALL_TYPE_DEV:
|
||||
typeTxt = '(dev mode)';
|
||||
break;
|
||||
default:
|
||||
typeTxt = '(NPM)';
|
||||
typeTxt = '(npm)';
|
||||
}
|
||||
installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
|
||||
|
||||
@@ -513,7 +526,12 @@ class ExtensionCommand {
|
||||
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`
|
||||
);
|
||||
}
|
||||
const pkgName = this.config.installedExtensions[installSpec].pkgName;
|
||||
const extRecord = this.config.installedExtensions[installSpec];
|
||||
if (extRecord.installType === INSTALL_TYPE_DEV) {
|
||||
this.log.warn(`Cannot uninstall ${this.type} "${installSpec}" because it is in development!`);
|
||||
return this.config.installedExtensions;
|
||||
}
|
||||
const pkgName = extRecord.pkgName;
|
||||
await npm.uninstallPackage(this.config.appiumHome, pkgName);
|
||||
await this.config.removeExtension(installSpec);
|
||||
this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
|
||||
@@ -771,11 +789,11 @@ class ExtensionCommand {
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtensionCommand;
|
||||
export {ExtensionCommand};
|
||||
export default ExtensionCliCommand;
|
||||
export {ExtensionCliCommand as ExtensionCommand};
|
||||
|
||||
/**
|
||||
* Options for the {@linkcode ExtensionCommand} constructor
|
||||
* Options for the {@linkcode ExtensionCliCommand} constructor
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef ExtensionCommandOptions
|
||||
* @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
|
||||
@@ -783,14 +801,15 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
|
||||
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
|
||||
*
|
||||
* @typedef ExtensionMetadata
|
||||
* @typedef ExtensionListMetadata
|
||||
* @property {boolean} installed - If `true`, the extension is installed
|
||||
* @property {string?} updateVersion - If the extension is installed, the version it can be updated to
|
||||
* @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
|
||||
* @property {boolean} upToDate - If the extension is installed and the latest
|
||||
* @property {string?} updateError - Update check error message (if present)
|
||||
* @property {string|null} updateVersion - If the extension is installed, the version it can be updated to
|
||||
* @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
|
||||
* @property {string} [updateError] - Update check error message (if present)
|
||||
* @property {boolean} [devMode] - If Appium is run from an extension's working copy
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -830,22 +849,24 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Possible return value for {@linkcode ExtensionCommand.list}
|
||||
* @typedef {Partial<InstalledExtensionListData> & {pkgName: string, installed: false}} UninstalledExtensionListData
|
||||
* Possible return value for {@linkcode ExtensionCliCommand.list}
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {Partial<ExtManifest<ExtType>> & Partial<ExtensionListMetadata>} ExtensionListData
|
||||
*/
|
||||
|
||||
/**
|
||||
* Possible return value for {@linkcode ExtensionCommand.list}
|
||||
* @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {ExtManifest<ExtType> & ExtensionListMetadata} InstalledExtensionListData
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return value of {@linkcode ExtensionCommand.list}.
|
||||
* @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
|
||||
* Return value of {@linkcode ExtensionCliCommand.list}.
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {Record<string,ExtensionListData<ExtType>>} ExtensionList
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand._run}.
|
||||
* Options for {@linkcode ExtensionCliCommand._run}.
|
||||
* @typedef RunOptions
|
||||
* @property {string} installSpec - name of the extension to run a script from
|
||||
* @property {string} scriptName - name of the script to run
|
||||
@@ -854,7 +875,7 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return value of {@linkcode ExtensionCommand._run}
|
||||
* Return value of {@linkcode ExtensionCliCommand._run}
|
||||
*
|
||||
* @typedef RunOutput
|
||||
* @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
|
||||
@@ -862,41 +883,41 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand._update}.
|
||||
* Options for {@linkcode ExtensionCliCommand._update}.
|
||||
* @typedef ExtensionUpdateOpts
|
||||
* @property {string} installSpec - the name of the extension to update
|
||||
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return value of {@linkcode ExtensionCommand._update}.
|
||||
* Return value of {@linkcode ExtensionCliCommand._update}.
|
||||
* @typedef ExtensionUpdateResult
|
||||
* @property {Record<string,Error>} errors - map of ext names to error objects
|
||||
* @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
|
||||
*/
|
||||
|
||||
/**
|
||||
* Part of result of {@linkcode ExtensionCommand._update}.
|
||||
* Part of result of {@linkcode ExtensionCliCommand._update}.
|
||||
* @typedef UpdateReport
|
||||
* @property {string} from - version the extension was updated from
|
||||
* @property {string} to - version the extension was updated to
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand._uninstall}.
|
||||
* Options for {@linkcode ExtensionCliCommand._uninstall}.
|
||||
* @typedef UninstallOpts
|
||||
* @property {string} installSpec - the name or spec of an extension to uninstall
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used by {@linkcode ExtensionCommand.getPostInstallText}
|
||||
* Used by {@linkcode ExtensionCliCommand.getPostInstallText}
|
||||
* @typedef ExtensionArgs
|
||||
* @property {string} extName - the name of an extension
|
||||
* @property {object} extData - the data for an installed extension
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand.installViaNpm}
|
||||
* Options for {@linkcode ExtensionCliCommand.installViaNpm}
|
||||
* @typedef InstallViaNpmArgs
|
||||
* @property {string} installSpec - the name or spec of an extension to install
|
||||
* @property {string} pkgName - the NPM package name of the extension
|
||||
@@ -905,7 +926,7 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
|
||||
* Object returned by {@linkcode ExtensionCliCommand.checkForExtensionUpdate}
|
||||
* @typedef PossibleUpdates
|
||||
* @property {string} current - current version
|
||||
* @property {string?} safeUpdate - version we can safely update to if it exists, or null
|
||||
@@ -913,7 +934,7 @@ export {ExtensionCommand};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand._install}
|
||||
* Options for {@linkcode ExtensionCliCommand._install}
|
||||
* @typedef InstallOpts
|
||||
* @property {string} installSpec - the name or spec of an extension to install
|
||||
* @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
|
||||
@@ -929,10 +950,11 @@ export {ExtensionCommand};
|
||||
* @typedef ListOptions
|
||||
* @property {boolean} showInstalled - whether should show only installed extensions
|
||||
* @property {boolean} showUpdates - whether should show available updates
|
||||
* @property {boolean} [verbose] - whether to show additional data from the extension
|
||||
*/
|
||||
|
||||
/**
|
||||
* Opts for {@linkcode ExtensionCommand.getInstallationReceipt}
|
||||
* Opts for {@linkcode ExtensionCliCommand.getInstallationReceipt}
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef GetInstallationReceiptOpts
|
||||
* @property {string} installPath
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable no-console */
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import {isExtensionCommandArgs} from '../utils';
|
||||
import DriverCommand from './driver-command';
|
||||
import PluginCommand from './plugin-command';
|
||||
import DriverCliCommand from './driver-command';
|
||||
import PluginCliCommand from './plugin-command';
|
||||
import {errAndQuit, JSON_SPACES} from './utils';
|
||||
|
||||
export const commandClasses = Object.freeze(
|
||||
/** @type {const} */ ({
|
||||
[DRIVER_TYPE]: DriverCommand,
|
||||
[PLUGIN_TYPE]: PluginCommand,
|
||||
[DRIVER_TYPE]: DriverCliCommand,
|
||||
[PLUGIN_TYPE]: PluginCliCommand,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ export {runExtensionCommand};
|
||||
|
||||
/**
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {ExtType extends DriverType ? Class<DriverCommand> : ExtType extends PluginType ? Class<PluginCommand> : never} ExtCommand
|
||||
* @typedef {ExtType extends DriverType ? Class<DriverCliCommand> : ExtType extends PluginType ? Class<PluginCliCommand> : never} ExtCommand
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,6 +101,13 @@ class ArgParser {
|
||||
try {
|
||||
const parsed = this.parser.parse_known_args(args);
|
||||
const [knownArgs, unknownArgs] = parsed;
|
||||
// XXX: you'd think that argparse, when given an alias for a subcommand,
|
||||
// would set this value to the original subcommand name, but it doesn't.
|
||||
if (knownArgs?.driverCommand === 'ls') {
|
||||
knownArgs.driverCommand = 'list';
|
||||
} else if (knownArgs?.pluginCommand === 'ls') {
|
||||
knownArgs.pluginCommand = 'list';
|
||||
}
|
||||
if (
|
||||
unknownArgs?.length &&
|
||||
(knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')
|
||||
@@ -210,13 +217,14 @@ class ArgParser {
|
||||
});
|
||||
const extensionArgs = getExtensionArgs();
|
||||
/**
|
||||
* @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string}[] }
|
||||
* @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string, aliases?: import('argparse').SubArgumentParserOptions['aliases']}[] }
|
||||
*/
|
||||
const parserSpecs = [
|
||||
{
|
||||
command: 'list',
|
||||
args: extensionArgs[type].list,
|
||||
help: `List available and installed ${type}s`,
|
||||
aliases: ['ls'],
|
||||
},
|
||||
{
|
||||
command: 'install',
|
||||
@@ -242,15 +250,18 @@ class ArgParser {
|
||||
},
|
||||
];
|
||||
|
||||
for (const {command, args, help} of parserSpecs) {
|
||||
const parser = extSubParsers.add_parser(command, {help});
|
||||
for (const {command, args, help, aliases} of parserSpecs) {
|
||||
const parser = extSubParsers.add_parser(command, {help, aliases: aliases ?? []});
|
||||
|
||||
ArgParser._patchExit(parser);
|
||||
|
||||
for (const [flagsOrNames, opts] of args) {
|
||||
// add_argument mutates params so make sure to send in copies instead
|
||||
// @ts-ignore
|
||||
parser.add_argument(...flagsOrNames, {...opts});
|
||||
if (flagsOrNames.length === 2) {
|
||||
parser.add_argument(flagsOrNames[0], flagsOrNames[1], {...opts});
|
||||
} else {
|
||||
parser.add_argument(flagsOrNames[0], {...opts});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
import ExtensionCommand from './extension-command';
|
||||
import ExtensionCliCommand from './extension-command';
|
||||
import {KNOWN_PLUGINS} from '../constants';
|
||||
|
||||
const REQ_PLUGIN_FIELDS = ['pluginName', 'mainClass'];
|
||||
|
||||
/**
|
||||
* @extends {ExtensionCommand<PluginType>}
|
||||
* @extends {ExtensionCliCommand<PluginType>}
|
||||
*/
|
||||
export default class PluginCommand extends ExtensionCommand {
|
||||
export default class PluginCliCommand extends ExtensionCliCommand {
|
||||
/**
|
||||
*
|
||||
* @param {import('./extension-command').ExtensionCommandOptions<PluginType>} opts
|
||||
@@ -110,7 +110,7 @@ export default class PluginCommand extends ExtensionCommand {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode PluginCommand.install}
|
||||
* Options for {@linkcode PluginCliCommand.install}
|
||||
* @typedef PluginInstallOpts
|
||||
* @property {string} plugin - the name or spec of a plugin to install
|
||||
* @property {InstallType} installType - how to install this plugin. One of the INSTALL_TYPES
|
||||
@@ -122,20 +122,20 @@ export default class PluginCommand extends ExtensionCommand {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode PluginCommand.uninstall}
|
||||
* Options for {@linkcode PluginCliCommand.uninstall}
|
||||
* @typedef PluginUninstallOpts
|
||||
* @property {string} plugin - the name or spec of a plugin to uninstall
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode PluginCommand.update}
|
||||
* Options for {@linkcode PluginCliCommand.update}
|
||||
* @typedef PluginUpdateOpts
|
||||
* @property {string} plugin - the name of the plugin to update
|
||||
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode PluginCommand.run}.
|
||||
* Options for {@linkcode PluginCliCommand.run}.
|
||||
* @typedef PluginRunOptions
|
||||
* @property {string} plugin - name of the plugin to run a script from
|
||||
* @property {string} scriptName - name of the script to run
|
||||
|
||||
@@ -68,4 +68,12 @@ export const EXT_SUBCOMMAND_RUN = 'run';
|
||||
/**
|
||||
* Current revision of the manifest (`extensions.yaml`) schema
|
||||
*/
|
||||
export const CURRENT_SCHEMA_REV = 3;
|
||||
export const CURRENT_SCHEMA_REV = 4;
|
||||
|
||||
/**
|
||||
* The default number of stack frames to show in a "long" stack trace, when enabled via `--long-stacktrace`
|
||||
* @remarks This value may be increased in the future.
|
||||
* @privateRemarks A value like `Infinity` may provide to have deleterious effects on
|
||||
* memory usage, perf, and/or log output, and higher limits may be difficult to scan.
|
||||
*/
|
||||
export const LONG_STACKTRACE_LIMIT = 100;
|
||||
|
||||
@@ -212,28 +212,29 @@ export class DriverConfig extends ExtensionConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {ExtensionType} T
|
||||
* @typedef {import('appium/types').ExtMetadata<T>} ExtMetadata
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {ExtensionType} T
|
||||
* @typedef {import('appium/types').ExtManifest<T>} ExtManifest
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('./manifest').Manifest} Manifest
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {ExtensionType} T
|
||||
* @typedef {import('appium/types').ExtRecord<T>} ExtRecord
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {ExtensionType} T
|
||||
* @typedef {import('appium/types').ExtName<T>} ExtName
|
||||
*/
|
||||
|
||||
@@ -244,7 +245,3 @@ export class DriverConfig extends ExtensionConfig {
|
||||
* @property {string} version
|
||||
* @property {string} driverName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').Capabilities} Capabilities
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import {util} from '@appium/support';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {satisfies} from 'semver';
|
||||
import {util} from '@appium/support';
|
||||
import {commandClasses} from '../cli/extension';
|
||||
import {APPIUM_VER} from '../config';
|
||||
import log from '../logger';
|
||||
@@ -13,17 +13,41 @@ import {
|
||||
registerSchema,
|
||||
} from '../schema/schema';
|
||||
|
||||
const INSTALL_TYPE_NPM = 'npm';
|
||||
const INSTALL_TYPE_LOCAL = 'local';
|
||||
const INSTALL_TYPE_GITHUB = 'github';
|
||||
const INSTALL_TYPE_GIT = 'git';
|
||||
/**
|
||||
* "npm" install type
|
||||
* Used when extension was installed by npm package name
|
||||
* @remarks _All_ extensions are installed _by_ `npm`, but only this one means the package name was
|
||||
* used to specify it
|
||||
*/
|
||||
export const INSTALL_TYPE_NPM = 'npm';
|
||||
/**
|
||||
* "local" install type
|
||||
* Used when extension was installed from a local path
|
||||
*/
|
||||
export const INSTALL_TYPE_LOCAL = 'local';
|
||||
/**
|
||||
* "github" install type
|
||||
* Used when extension was installed via GitHub URL
|
||||
*/
|
||||
export const INSTALL_TYPE_GITHUB = 'github';
|
||||
/**
|
||||
* "git" install type
|
||||
* Used when extensions was installed via Git URL
|
||||
*/
|
||||
export const INSTALL_TYPE_GIT = 'git';
|
||||
/**
|
||||
* "dev" install type
|
||||
* Used when automatically detected as a working copy
|
||||
*/
|
||||
export const INSTALL_TYPE_DEV = 'dev';
|
||||
|
||||
/** @type {Set<InstallType>} */
|
||||
const INSTALL_TYPES = new Set([
|
||||
export const INSTALL_TYPES = new Set([
|
||||
INSTALL_TYPE_GIT,
|
||||
INSTALL_TYPE_GITHUB,
|
||||
INSTALL_TYPE_LOCAL,
|
||||
INSTALL_TYPE_NPM,
|
||||
INSTALL_TYPE_DEV,
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -54,7 +78,7 @@ export class ExtensionConfig {
|
||||
manifest;
|
||||
|
||||
/**
|
||||
* @type {ExtensionListData|undefined}
|
||||
* @type {import('../cli/extension-command').ExtensionList<ExtType>|undefined}
|
||||
*/
|
||||
#listDataCache;
|
||||
|
||||
@@ -239,7 +263,7 @@ export class ExtensionConfig {
|
||||
* This is an expensive operation, so the result is cached. Currently, there is no
|
||||
* use case for invalidating the cache.
|
||||
* @protected
|
||||
* @returns {Promise<ExtensionListData>}
|
||||
* @returns {Promise<import('../cli/extension-command').ExtensionList<ExtType>>}
|
||||
*/
|
||||
async getListData() {
|
||||
if (this.#listDataCache) {
|
||||
@@ -300,7 +324,10 @@ export class ExtensionConfig {
|
||||
|
||||
if (_.isString(appiumVersion) && !satisfies(APPIUM_VER, appiumVersion)) {
|
||||
const listData = await this.getListData();
|
||||
const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
|
||||
const extListData =
|
||||
/** @type {import('../cli/extension-command').ExtensionListData<ExtType>} */ (
|
||||
listData[extName]
|
||||
);
|
||||
if (extListData?.installed) {
|
||||
const {updateVersion, upToDate} = extListData;
|
||||
if (!upToDate) {
|
||||
@@ -319,7 +346,10 @@ export class ExtensionConfig {
|
||||
}
|
||||
} else if (!_.isString(appiumVersion)) {
|
||||
const listData = await this.getListData();
|
||||
const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
|
||||
const extListData =
|
||||
/** @type {import('../cli/extension-command').InstalledExtensionListData<ExtType>} */ (
|
||||
listData[extName]
|
||||
);
|
||||
if (!extListData?.upToDate && extListData?.updateVersion) {
|
||||
warnings.push(
|
||||
createPeerWarning(
|
||||
@@ -544,6 +574,7 @@ export class ExtensionConfig {
|
||||
delete require.cache[reqResolved];
|
||||
}
|
||||
log.debug(`Requiring ${this.extensionType} at ${reqPath}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const MainClass = require(reqPath)[mainClass];
|
||||
if (!MainClass) {
|
||||
throw new ReferenceError(
|
||||
@@ -619,8 +650,6 @@ export class ExtensionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GITHUB, INSTALL_TYPES};
|
||||
|
||||
/**
|
||||
* An issue with the {@linkcode ExtManifest} for a particular extension.
|
||||
*
|
||||
@@ -640,8 +669,6 @@ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GIT
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('./manifest').Manifest} Manifest
|
||||
* @typedef {import('../cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('../cli/extension-command').InstalledExtensionListData} InstalledExtensionListData
|
||||
* @typedef {import('appium/types').InstallType} InstallType
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import log from '../logger';
|
||||
|
||||
/**
|
||||
@@ -12,6 +12,11 @@ import log from '../logger';
|
||||
*/
|
||||
const SCHEMA_REV_3 = 3;
|
||||
|
||||
/**
|
||||
* Constant for v4 of the schema rev.
|
||||
*/
|
||||
const SCHEMA_REV_4 = 4;
|
||||
|
||||
/**
|
||||
* Collection of functions to migrate from one version to another.
|
||||
*
|
||||
@@ -20,7 +25,7 @@ const SCHEMA_REV_3 = 3;
|
||||
* itself will happen within {@linkcode Manifest.syncWithInstalledExtensions}; the extensions
|
||||
* will be checked and the manifest file updated per the state of the filesystem.
|
||||
*
|
||||
* @type {{[P in keyof ManifestDataVersions]?: Migration}}
|
||||
* @type { {[P in keyof ManifestDataVersions]?: Migration} }
|
||||
*/
|
||||
const Migrations = {
|
||||
/**
|
||||
@@ -32,19 +37,32 @@ const Migrations = {
|
||||
* @type {Migration}
|
||||
*/
|
||||
[SCHEMA_REV_3]: (manifest) => {
|
||||
let shouldSync = false;
|
||||
/** @type {Array<ExtManifest<PluginType>|ExtManifest<DriverType>>} */
|
||||
const allExtData = [
|
||||
...Object.values(manifest.getExtensionData(DRIVER_TYPE)),
|
||||
...Object.values(manifest.getExtensionData(PLUGIN_TYPE)),
|
||||
];
|
||||
for (const metadata of allExtData) {
|
||||
if (!('installPath' in metadata)) {
|
||||
shouldSync = true;
|
||||
break;
|
||||
}
|
||||
return allExtData.some((metadata) => !('installPath' in metadata));
|
||||
},
|
||||
/**
|
||||
* Updates installed extensions to use `InstallType` of `dev` if appropriate.
|
||||
*
|
||||
* Previously, these types of extensions (automatically discovered) would use the default `InstallType` of `npm`, so we need
|
||||
* to refresh any with this install type.
|
||||
*
|
||||
* This should only happen once; we do not want to re-check everything with `npm` install type
|
||||
* every time.
|
||||
* @type {Migration}
|
||||
*/
|
||||
[SCHEMA_REV_4]: (manifest) => {
|
||||
if (manifest.schemaRev < SCHEMA_REV_4) {
|
||||
const allExtData = [
|
||||
...Object.values(manifest.getExtensionData(DRIVER_TYPE)),
|
||||
...Object.values(manifest.getExtensionData(PLUGIN_TYPE)),
|
||||
];
|
||||
return allExtData.some((metadata) => metadata.installType === 'npm');
|
||||
}
|
||||
return shouldSync;
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,7 +79,7 @@ const Migrations = {
|
||||
* @returns {boolean} Whether the data was modified
|
||||
*/
|
||||
function setSchemaRev(manifest, version) {
|
||||
if (manifest.schemaRev ?? 0 < version) {
|
||||
if ((manifest.schemaRev ?? 0) < version) {
|
||||
manifest.setSchemaRev(version);
|
||||
return true;
|
||||
}
|
||||
@@ -78,15 +96,13 @@ function setSchemaRev(manifest, version) {
|
||||
*/
|
||||
export async function migrate(manifest) {
|
||||
let didChange = false;
|
||||
for await (const [v, migration] of Object.entries(Migrations)) {
|
||||
const version = /** @type {keyof ManifestDataVersions} */ (Number(v));
|
||||
for await (const migration of Object.values(Migrations)) {
|
||||
didChange = (await migration(manifest)) || didChange;
|
||||
didChange = setSchemaRev(manifest, version) || didChange;
|
||||
}
|
||||
|
||||
didChange = setSchemaRev(manifest, CURRENT_SCHEMA_REV) || didChange;
|
||||
if (didChange) {
|
||||
// this is not _technically_ true, since we don't actually write the file here.
|
||||
log.info(`Upgraded extension manifest to schema v${manifest.schemaRev}`);
|
||||
log.debug(`Upgraded extension manifest to schema v${manifest.schemaRev}`);
|
||||
}
|
||||
|
||||
return didChange;
|
||||
|
||||
@@ -10,7 +10,7 @@ import path from 'path';
|
||||
import YAML from 'yaml';
|
||||
import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import log from '../logger';
|
||||
import {INSTALL_TYPE_NPM} from './extension-config';
|
||||
import {INSTALL_TYPE_NPM, INSTALL_TYPE_DEV} from './extension-config';
|
||||
import {packageDidChange} from './package-changed';
|
||||
import {migrate} from './manifest-migrations';
|
||||
|
||||
@@ -148,27 +148,33 @@ export class Manifest {
|
||||
* Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`.
|
||||
*
|
||||
* Maintains one instance per value of `appiumHome`.
|
||||
* @param {string} appiumHome - Path to `APPIUM_HOME`
|
||||
* @returns {Manifest}
|
||||
*/
|
||||
static getInstance = _.memoize(function _getInstance(appiumHome) {
|
||||
return new Manifest(appiumHome);
|
||||
});
|
||||
static getInstance = _.memoize(
|
||||
/**
|
||||
* @param {string} appiumHome - Path to `APPIUM_HOME`
|
||||
* @returns {Manifest}
|
||||
*/
|
||||
function _getInstance(appiumHome) {
|
||||
return new Manifest(appiumHome);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Searches `APPIUM_HOME` for installed extensions and adds them to the manifest.
|
||||
* @param {boolean} hasAppiumDependency - This affects whether or not the "dev" `InstallType` is used
|
||||
* @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise.
|
||||
*/
|
||||
async syncWithInstalledExtensions() {
|
||||
async syncWithInstalledExtensions(hasAppiumDependency = false) {
|
||||
// this could be parallelized, but we can't use fs.walk as an async iterator
|
||||
let didChange = false;
|
||||
|
||||
/**
|
||||
* Listener for the `match` event of a `glob` instance
|
||||
* @param {string} filepath - Path to a `package.json`
|
||||
* @param {boolean} [devType] - If `true`, this is an extension in "dev mode"
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const onMatch = async (filepath) => {
|
||||
const onMatch = async (filepath, devType = false) => {
|
||||
try {
|
||||
const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'));
|
||||
if (isExtension(pkg)) {
|
||||
@@ -188,7 +194,8 @@ export class Manifest {
|
||||
) {
|
||||
log.info(`Discovered installed ${extType} "${name}"`);
|
||||
}
|
||||
const changed = this.addExtensionFromPackage(pkg, filepath);
|
||||
const installType = devType && hasAppiumDependency ? INSTALL_TYPE_DEV : INSTALL_TYPE_NPM;
|
||||
const changed = this.addExtensionFromPackage(pkg, filepath, installType);
|
||||
didChange = didChange || changed;
|
||||
}
|
||||
} catch {}
|
||||
@@ -199,8 +206,9 @@ export class Manifest {
|
||||
* @type {Promise<void>[]}
|
||||
*/
|
||||
const queue = [
|
||||
// look at `package.json` in `APPIUM_HOME` only
|
||||
onMatch(path.join(this.#appiumHome, 'package.json')),
|
||||
// look at `package.json` in `APPIUM_HOME` only.
|
||||
// this causes extensions in "dev mode" to be automatically found
|
||||
onMatch(path.join(this.#appiumHome, 'package.json'), true),
|
||||
];
|
||||
|
||||
// add dependencies to the queue
|
||||
@@ -252,9 +260,10 @@ export class Manifest {
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtPackageJson<ExtType>} pkgJson
|
||||
* @param {string} pkgPath
|
||||
* @param {typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_DEV} [installType]
|
||||
* @returns {boolean} - `true` if this method did anything.
|
||||
*/
|
||||
addExtensionFromPackage(pkgJson, pkgPath) {
|
||||
addExtensionFromPackage(pkgJson, pkgPath, installType = INSTALL_TYPE_NPM) {
|
||||
const extensionPath = path.dirname(pkgPath);
|
||||
|
||||
/**
|
||||
@@ -264,7 +273,7 @@ export class Manifest {
|
||||
pkgName: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
appiumVersion: pkgJson.peerDependencies?.appium,
|
||||
installType: INSTALL_TYPE_NPM,
|
||||
installType,
|
||||
installSpec: `${pkgJson.name}@${pkgJson.version}`,
|
||||
installPath: extensionPath,
|
||||
};
|
||||
@@ -434,6 +443,8 @@ export class Manifest {
|
||||
shouldWrite = await migrate(this);
|
||||
}
|
||||
|
||||
const hasAppiumDependency = await env.hasAppiumDependency(this.appiumHome);
|
||||
|
||||
/**
|
||||
* we still may want to sync with installed extensions even if we have a
|
||||
* new file. right now this is limited to the following cases:
|
||||
@@ -445,13 +456,9 @@ export class Manifest {
|
||||
* It may also make sense to sync with the extensions in an arbitrary
|
||||
* `APPIUM_HOME`, but we don't do that here.
|
||||
*/
|
||||
if (
|
||||
shouldWrite ||
|
||||
((await env.hasAppiumDependency(this.appiumHome)) &&
|
||||
(await packageDidChange(this.appiumHome)))
|
||||
) {
|
||||
if (shouldWrite || (hasAppiumDependency && (await packageDidChange(this.appiumHome)))) {
|
||||
log.debug('Discovering newly installed extensions...');
|
||||
shouldWrite = (await this.syncWithInstalledExtensions()) || shouldWrite;
|
||||
shouldWrite = (await this.syncWithInstalledExtensions(hasAppiumDependency)) || shouldWrite;
|
||||
}
|
||||
|
||||
if (shouldWrite) {
|
||||
|
||||
+66
-17
@@ -2,9 +2,8 @@
|
||||
|
||||
import {init as logsinkInit} from './logsink'; // this import needs to come first since it sets up global npmlog
|
||||
import logger from './logger'; // logger needs to remain second
|
||||
// @ts-ignore
|
||||
import {routeConfiguringFunction as makeRouter, server as baseServer} from '@appium/base-driver';
|
||||
import {logger as logFactory, util, env} from '@appium/support';
|
||||
import {logger as logFactory, util, env, fs} from '@appium/support';
|
||||
import {asyncify} from 'asyncbox';
|
||||
import _ from 'lodash';
|
||||
import {AppiumDriver} from './appium';
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
} from './config';
|
||||
import {readConfigFile} from './config-file';
|
||||
import {loadExtensions, getActivePlugins, getActiveDrivers} from './extension';
|
||||
import {SERVER_SUBCOMMAND} from './constants';
|
||||
import {SERVER_SUBCOMMAND, LONG_STACKTRACE_LIMIT} from './constants';
|
||||
import registerNode from './grid-register';
|
||||
import {getDefaultsForSchema, validate} from './schema/schema';
|
||||
import {
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
isPluginCommandArgs,
|
||||
isServerCommandArgs,
|
||||
} from './utils';
|
||||
import os from 'node:os';
|
||||
|
||||
const {resolveAppiumHome} = env;
|
||||
|
||||
@@ -47,7 +47,7 @@ async function preflightChecks(args, throwInsteadOfExit = false) {
|
||||
checkNodeOk();
|
||||
await checkNpmOk();
|
||||
if (args.longStacktrace) {
|
||||
require('longjohn').async_trace_limit = -1;
|
||||
Error.stackTraceLimit = LONG_STACKTRACE_LIMIT;
|
||||
}
|
||||
if (args.showBuildInfo) {
|
||||
await showBuildInfo();
|
||||
@@ -114,17 +114,6 @@ async function logStartupInfo(args) {
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the address and port the server is listening on
|
||||
* @param {string} address - Address
|
||||
* @param {number} port - Port
|
||||
* @returns {void}
|
||||
*/
|
||||
function logServerPort(address, port) {
|
||||
let logMessage = `Appium REST http interface listener started on ` + `${address}:${port}`;
|
||||
logger.info(logMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of `updateServer` functions from all extensions
|
||||
* @param {DriverNameMap} driverClasses
|
||||
@@ -150,6 +139,52 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and validates appium home path folder
|
||||
*
|
||||
* @param {string} name The name of the appium home source (needed for error messages)
|
||||
* @param {string} appiumHome The actual value to be verified
|
||||
* @returns {Promise<string>} Same appiumHome value
|
||||
* @throws {Error} If the validation has failed
|
||||
*/
|
||||
async function prepareAppiumHome(name, appiumHome) {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(appiumHome);
|
||||
} catch (e) {
|
||||
let err = e;
|
||||
if (e.code === 'ENOENT') {
|
||||
try {
|
||||
await fs.mkdir(appiumHome, {recursive: true});
|
||||
return appiumHome;
|
||||
} catch (e1) {
|
||||
err = e1;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`The path '${appiumHome}' provided in the ${name} must point ` +
|
||||
`to a valid folder writeable for the current user account '${os.userInfo().username}'. ` +
|
||||
`Original error: ${err.message}`
|
||||
);
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(
|
||||
`The path '${appiumHome}' provided in the ${name} must point to a valid folder`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await fs.access(appiumHome, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`The folder path '${appiumHome}' provided in the ${name} must be ` +
|
||||
`writeable for the current user account '${os.userInfo().username}. ` +
|
||||
`Original error: ${e.message}`
|
||||
);
|
||||
}
|
||||
return appiumHome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Appium, but does not start the server.
|
||||
*
|
||||
@@ -169,6 +204,13 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
|
||||
*/
|
||||
async function init(args) {
|
||||
const appiumHome = args?.appiumHome ?? (await resolveAppiumHome());
|
||||
let appiumHomeSourceName = 'autodetected appium home path';
|
||||
if (!_.isNil(args?.appiumHome)) {
|
||||
appiumHomeSourceName = 'appiumHome config value';
|
||||
} else if (process.env.APPIUM_HOME) {
|
||||
appiumHomeSourceName = 'APPIUM_HOME environment variable';
|
||||
}
|
||||
await prepareAppiumHome(appiumHomeSourceName, appiumHome);
|
||||
|
||||
adjustNodePath();
|
||||
|
||||
@@ -245,7 +287,11 @@ async function init(args) {
|
||||
}
|
||||
}
|
||||
|
||||
const appiumDriver = new AppiumDriver(serverArgs);
|
||||
const appiumDriver = new AppiumDriver(
|
||||
/** @type {import('@appium/types').DriverOpts<import('./appium').AppiumDriverConstraints>} */ (
|
||||
serverArgs
|
||||
)
|
||||
);
|
||||
// set the config on the umbrella driver so it can match drivers to caps
|
||||
appiumDriver.driverConfig = driverConfig;
|
||||
await preflightChecks(serverArgs, throwInsteadOfExit);
|
||||
@@ -371,7 +417,10 @@ async function main(args) {
|
||||
});
|
||||
}
|
||||
|
||||
logServerPort(parsedArgs.address, parsedArgs.port);
|
||||
logger.info(
|
||||
`Appium REST http interface listener started on ${parsedArgs.address}:${parsedArgs.port}${parsedArgs.basePath}`
|
||||
);
|
||||
|
||||
driverConfig.print();
|
||||
pluginConfig.print([...pluginClasses.values()]);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ function makeDescription(schema) {
|
||||
* as understood by `argparse`.
|
||||
* @param {AppiumJSONSchema} subSchema - JSON schema for the option
|
||||
* @param {ArgSpec} argSpec - Argument spec tuple
|
||||
* @returns {[string[], import('argparse').ArgumentOptions]} Tuple of flag and options
|
||||
* @returns {[[string]|[string, string], import('argparse').ArgumentOptions]} Tuple of flag and options
|
||||
*/
|
||||
function subSchemaToArgDef(subSchema, argSpec) {
|
||||
let {type, appiumCliAliases, appiumCliTransformer, enum: enumValues} = subSchema;
|
||||
@@ -207,7 +207,10 @@ function subSchemaToArgDef(subSchema, argSpec) {
|
||||
}
|
||||
}
|
||||
|
||||
return [aliases, argOpts];
|
||||
// TODO: argparse only accepts the command name and a single alias; any extra aliases
|
||||
// will be silently discarded.
|
||||
const finalAliases = /** @type {[string]|[string, string]} */ (aliases);
|
||||
return [finalAliases, argOpts];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +227,7 @@ export function toParserArgs() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {string|number} T
|
||||
* @typedef {import('ajv/dist/types').FormatValidator<T>} FormatValidator<T>
|
||||
*/
|
||||
|
||||
|
||||
@@ -223,7 +223,8 @@ function adjustNodePath() {
|
||||
// ! statements in client modules. It uses a private API though,
|
||||
// ! so it could break (maybe, eventually).
|
||||
// See https://gist.github.com/branneman/8048520#7-the-hack
|
||||
// @ts-ignore
|
||||
// @ts-ignore see above comment
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('module').Module._initPaths();
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -368,21 +369,18 @@ export {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').NSCapabilities<C, Extra>} NSCapabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.0.0-beta.57",
|
||||
"version": "2.0.0-beta.63",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,13 +44,14 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build:docs": "node docs/scripts/build-docs.js",
|
||||
"build:docs:preview": "cross-env APPIUM_DOCS_PREVIEW=1 npm run build:docs",
|
||||
"build:docs:assets": "node docs/scripts/copy-assets.js",
|
||||
"build:docs:cli": "node docs/scripts/gen-cli-args-docs.js",
|
||||
"build:docs:reference": "node docs/scripts/build-reference.js",
|
||||
"dev:docs": "npm run build:docs:assets && npm run dev:docs:en",
|
||||
"dev:docs:en": "mkdocs serve -f ./docs/mkdocs-en.yml",
|
||||
"dev:docs:ja": "mkdocs serve -f ./docs/mkdocs-ja.yml",
|
||||
"dev:docs:zh": "mkdocs serve -f ./docs/mkdocs-zh.yml",
|
||||
"dev:docs:en": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-en.yml",
|
||||
"dev:docs:ja": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-ja.yml",
|
||||
"dev:docs:zh": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-zh.yml",
|
||||
"postinstall": "node ./scripts/autoinstall-extensions.js",
|
||||
"publish:docs": "cross-env APPIUM_DOCS_PUBLISH=1 npm run build:docs",
|
||||
"test": "npm run test:unit",
|
||||
@@ -59,12 +60,12 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.3.3",
|
||||
"@appium/base-plugin": "^2.2.3",
|
||||
"@appium/docutils": "^0.3.0",
|
||||
"@appium/base-driver": "^9.3.6",
|
||||
"@appium/base-plugin": "^2.2.6",
|
||||
"@appium/docutils": "^0.3.6",
|
||||
"@appium/schema": "^0.2.6",
|
||||
"@appium/support": "^3.1.7",
|
||||
"@appium/types": "^0.10.1",
|
||||
"@appium/support": "^3.1.10",
|
||||
"@appium/types": "^0.10.4",
|
||||
"@sidvind/better-ajv-errors": "2.1.0",
|
||||
"@types/argparse": "2.0.10",
|
||||
"@types/bluebird": "3.5.38",
|
||||
@@ -77,22 +78,21 @@
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.0",
|
||||
"asyncbox": "2.9.4",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.3.5",
|
||||
"bluebird": "3.7.2",
|
||||
"cross-env": "7.0.3",
|
||||
"find-up": "5.0.0",
|
||||
"glob": "8.1.0",
|
||||
"lilconfig": "2.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"longjohn": "0.2.12",
|
||||
"npmlog": "7.0.1",
|
||||
"ora": "5.4.1",
|
||||
"package-changed": "2.0.0",
|
||||
"package-changed": "3.0.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.8",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.6.1",
|
||||
"type-fest": "3.8.0",
|
||||
"winston": "3.8.2",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.2.1"
|
||||
@@ -105,7 +105,7 @@
|
||||
"access": "public",
|
||||
"tag": "next"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
|
||||
"typedoc": {
|
||||
"entryPoint": "./lib/main.js"
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('Plugin CLI', function () {
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionList} ExtensionListData
|
||||
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
|
||||
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
|
||||
*/
|
||||
|
||||
@@ -182,7 +182,7 @@ export function formatAppiumArgErrorOutput(stderr) {
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionList} ExtensionListData
|
||||
* @typedef {import('teen_process').ExecError} ExecError
|
||||
* @typedef {import('teen_process').TeenProcessExecOptions} TeenProcessExecOptions
|
||||
* @typedef {import('teen_process').TeenProcessExecResult<string>} TeenProcessExecResult
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('manifest handling', function () {
|
||||
});
|
||||
|
||||
it('should update the manifest file to the latest schema revision', function () {
|
||||
expect(manifest.schemaRev).to.equal(3);
|
||||
expect(manifest.schemaRev).to.equal(CURRENT_SCHEMA_REV);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import {migrate} from '../../../lib/extension/manifest-migrations';
|
||||
import {DRIVER_TYPE} from '../../../lib/constants';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('manifest-migrations', function () {
|
||||
describe('when no installPath property present in manifest', function () {
|
||||
it('should trigger refresh', async function () {
|
||||
const manifest = Manifest.getInstance(process.cwd());
|
||||
// do not explicitly set the schema rev lower here, since that will trigger
|
||||
manifest.setExtension(
|
||||
DRIVER_TYPE,
|
||||
'derp',
|
||||
// @ts-expect-error - old manifest version
|
||||
{
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installSpec: 'derp',
|
||||
installType: 'local',
|
||||
appiumVersion: '2.0.0',
|
||||
}
|
||||
);
|
||||
|
||||
await expect(migrate(manifest)).to.eventually.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when installPath property present in manifest', function () {
|
||||
it('should not trigger refresh', async function () {
|
||||
const manifest = Manifest.getInstance(process.cwd());
|
||||
// do not explicitly set the schema rev lower here, since that will trigger
|
||||
manifest.setExtension(DRIVER_TYPE, 'derp', {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installPath: '/path/to/thing',
|
||||
installType: 'local',
|
||||
installSpec: 'derp',
|
||||
appiumVersion: '2.0.0',
|
||||
});
|
||||
|
||||
await expect(migrate(manifest)).to.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an installType is "npm" and the rev is old', function () {
|
||||
it('should trigger refresh', async function () {
|
||||
const manifest = Manifest.getInstance(process.cwd());
|
||||
manifest.setSchemaRev(3); // this will trigger a refresh, but there's no way to tell _why_, which may be bad. YAGNI?
|
||||
manifest.setExtension(DRIVER_TYPE, 'derp', {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installType: 'npm',
|
||||
installPath: '/path/to/thing',
|
||||
installSpec: 'derp',
|
||||
appiumVersion: '2.0.0',
|
||||
});
|
||||
|
||||
await expect(migrate(manifest)).to.eventually.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no installType is "npm"', function () {
|
||||
it('should not trigger refresh', async function () {
|
||||
const manifest = Manifest.getInstance(process.cwd());
|
||||
// do not explicitly set the schema rev lower here, since that will trigger
|
||||
manifest.setExtension(DRIVER_TYPE, 'derp', {
|
||||
version: '1.0.0',
|
||||
automationName: 'Derp',
|
||||
mainClass: 'SomeClass',
|
||||
pkgName: 'derp',
|
||||
platformNames: ['dogs', 'cats'],
|
||||
installType: 'local',
|
||||
installPath: '/path/to/thing',
|
||||
installSpec: 'derp',
|
||||
appiumVersion: '2.0.0',
|
||||
});
|
||||
|
||||
await expect(migrate(manifest)).to.eventually.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,10 @@ describe('Manifest', function () {
|
||||
let overrides;
|
||||
({MockPackageChanged, MockAppiumSupport, MockGlob, overrides, sandbox} = initMocks());
|
||||
MockAppiumSupport.fs.readFile.resolves(yamlFixture);
|
||||
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), overrides));
|
||||
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), {
|
||||
...overrides,
|
||||
'../../../lib/extension/manifest-migrations.js': {migrate: sandbox.stub().resolves()},
|
||||
}));
|
||||
|
||||
Manifest.getInstance.cache = new Map();
|
||||
});
|
||||
|
||||
@@ -264,6 +264,14 @@ describe('parser', function () {
|
||||
const args = p.parseArgs([DRIVER_TYPE, 'list', '--updates']);
|
||||
args.showUpdates.should.eql(true);
|
||||
});
|
||||
it('should allow "ls" as an alias for "list"', function () {
|
||||
const args = p.parseArgs([DRIVER_TYPE, 'ls']);
|
||||
args.subcommand.should.eql(DRIVER_TYPE);
|
||||
args.driverCommand.should.eql('list');
|
||||
args.showInstalled.should.eql(false);
|
||||
args.showUpdates.should.eql(false);
|
||||
args.json.should.eql(false);
|
||||
});
|
||||
});
|
||||
describe('install', function () {
|
||||
it('should not allow an empty argument list', function () {
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
|
||||
import * as ManifestV2 from './base';
|
||||
import * as ManifestV3 from './v3';
|
||||
|
||||
import * as ManifestV4 from './v4';
|
||||
// add `import * as ManifestV<new-version> from './v<new-version>';` above
|
||||
|
||||
export * from './v3';
|
||||
export * from './v4';
|
||||
// replace above line with `export * from './v<new-version>';`
|
||||
|
||||
export {ManifestV2, ManifestV3};
|
||||
export {ManifestV2, ManifestV3, ManifestV4};
|
||||
|
||||
export interface ManifestDataVersions {
|
||||
2: ManifestV2.ManifestData;
|
||||
3: ManifestV3.ManifestData;
|
||||
4: ManifestV4.ManifestData;
|
||||
}
|
||||
// append to this interface your new version of `ManifestData`
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {DriverType, ExtensionType, PluginType} from '@appium/types';
|
||||
import {SchemaObject} from 'ajv';
|
||||
import {PackageJson, SetRequired} from 'type-fest';
|
||||
|
||||
/**
|
||||
* One of the possible extension installation stratgies
|
||||
*/
|
||||
export type InstallType = 'npm' | 'git' | 'local' | 'github' | 'dev';
|
||||
|
||||
export interface InternalMetadata {
|
||||
/**
|
||||
* Package name of extension
|
||||
*
|
||||
* `name` from its `package.json`
|
||||
*/
|
||||
pkgName: string;
|
||||
/**
|
||||
* Version of extension
|
||||
*
|
||||
* `version` from its `package.json`
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* The method in which the user installed the extension (the `source` CLI arg)
|
||||
*/
|
||||
installType: InstallType;
|
||||
/**
|
||||
* Whatever the user typed as the extension to install. May be derived from `package.json`
|
||||
*/
|
||||
installSpec: string;
|
||||
/**
|
||||
* Maximum version of Appium that this extension is compatible with.
|
||||
*
|
||||
* If `undefined`, we'll try anyway.
|
||||
*/
|
||||
appiumVersion?: string;
|
||||
/**
|
||||
* Path to the extension's root directory
|
||||
*/
|
||||
installPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the `appium.schema` property in an extension's `package.json` (if it exists)
|
||||
*/
|
||||
export type ExtSchemaMetadata = string | (SchemaObject & {[key: number]: never});
|
||||
|
||||
/**
|
||||
* Manifest data shared by all extensions, as contained in `package.json`
|
||||
*/
|
||||
export interface CommonExtMetadata {
|
||||
/**
|
||||
* The main class of the extension.
|
||||
*
|
||||
* The extension must export this class by name.
|
||||
*/
|
||||
mainClass: string;
|
||||
|
||||
/**
|
||||
* Lookup table of scripts to run via `appium <driver|plugin> run <script>` keyed by name.
|
||||
*/
|
||||
scripts?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Schema describing configuration options (and CLI args) for the extension.
|
||||
*
|
||||
* Can also just be a path (relative to the extension root) to an external JSON schema file.
|
||||
*/
|
||||
schema?: ExtSchemaMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver-specific manifest data as stored in a driver's `package.json`
|
||||
*/
|
||||
export interface DriverMetadata {
|
||||
/**
|
||||
* Automation name of the driver
|
||||
*/
|
||||
automationName: string;
|
||||
/**
|
||||
* Platforms the driver supports
|
||||
*/
|
||||
platformNames: string[];
|
||||
/**
|
||||
* Short name of the driver (displayed in `appium list`, etc.)
|
||||
*/
|
||||
driverName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin-specific manifest data as stored in a plugin's `package.json`
|
||||
*/
|
||||
export interface PluginMetadata {
|
||||
/**
|
||||
* Short name of the plugin (displayed in `appium list`, etc.)
|
||||
*/
|
||||
pluginName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic extension metadata as stored in the `appium` prop of an extension's `package.json`.
|
||||
*/
|
||||
export type ExtMetadata<ExtType extends ExtensionType> = (ExtType extends DriverType
|
||||
? DriverMetadata
|
||||
: ExtType extends PluginType
|
||||
? PluginMetadata
|
||||
: never) &
|
||||
CommonExtMetadata;
|
||||
|
||||
/**
|
||||
* Combination of external + internal extension data with `driverName`/`pluginName` removed (it becomes a key in an {@linkcode ExtRecord} object).
|
||||
* Part of `extensions.yaml`.
|
||||
*/
|
||||
export type ExtManifest<ExtType extends ExtensionType> = Omit<
|
||||
ExtMetadata<ExtType>,
|
||||
'driverName' | 'pluginName'
|
||||
> &
|
||||
InternalMetadata;
|
||||
|
||||
/**
|
||||
* Lookup of extension name to {@linkcode ExtManifest}.
|
||||
* @see {ManifestData}
|
||||
*/
|
||||
export type ExtRecord<ExtType extends ExtensionType> = Record<string, ExtManifest<ExtType>>;
|
||||
|
||||
/**
|
||||
* The shape of the `extensions.yaml` file
|
||||
*/
|
||||
export interface ManifestData {
|
||||
drivers: ExtRecord<DriverType>;
|
||||
plugins: ExtRecord<PluginType>;
|
||||
schemaRev: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of an installed extension, as it appears in `extensions.yaml`
|
||||
* (as a property name under `drivers` or `plugins`)
|
||||
*/
|
||||
export type ExtName<ExtType extends ExtensionType> = keyof ExtRecord<ExtType>;
|
||||
|
||||
/**
|
||||
* A `package.json` containing extension metadata.
|
||||
* Must have the following properties:
|
||||
* - `name`: the name of the extension
|
||||
* - `version`: the version of the extension
|
||||
* - `appium`: the metadata for the extension
|
||||
* - `peerDependencies.appium`: the maximum compatible version of Appium
|
||||
*/
|
||||
export type ExtPackageJson<ExtType extends ExtensionType> = SetRequired<
|
||||
PackageJson,
|
||||
'name' | 'version'
|
||||
> & {
|
||||
appium: ExtMetadata<ExtType>;
|
||||
peerDependencies: {appium: string; [key: string]: string};
|
||||
};
|
||||
|
||||
/**
|
||||
* A transient format between installation and insertion of extension metadata into the manifest.
|
||||
*/
|
||||
export type ExtInstallReceipt<ExtType extends ExtensionType> = ExtMetadata<ExtType> &
|
||||
InternalMetadata;
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [9.3.6](https://github.com/appium/appium/compare/@appium/base-driver@9.3.5...@appium/base-driver@9.3.6) (2023-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
|
||||
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [9.3.5](https://github.com/appium/appium/compare/@appium/base-driver@9.3.4...@appium/base-driver@9.3.5) (2023-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [9.3.4](https://github.com/appium/appium/compare/@appium/base-driver@9.3.3...@appium/base-driver@9.3.4) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
|
||||
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
|
||||
* **base-driver,base-plugin,types:** update PluginCommand and DriverCommand types ([0dcd5fa](https://github.com/appium/appium/commit/0dcd5fa371af523c6527e55de4cff6cd472fde22))
|
||||
* **base-driver,types:** fix websocket-related types on AppiumServer ([34891f5](https://github.com/appium/appium/commit/34891f56572f18dd740558b2348d8818680dc709))
|
||||
* **base-driver:** fix timeout mixin to use proper context types ([51a8f2f](https://github.com/appium/appium/commit/51a8f2fc347f0b4e222ca2e8dc92fa4bdf44d1e3))
|
||||
* **base-driver:** misc type fixes ([d303527](https://github.com/appium/appium/commit/d303527c3da4f657a28b97a2d82eb1a709c6b9bc))
|
||||
* **basedriver:** convert main driver implementation to typescript ([deb631b](https://github.com/appium/appium/commit/deb631b4562a0f99e4061c89a7ad21b0621f8a47)), closes [#18379](https://github.com/appium/appium/issues/18379) [#18379](https://github.com/appium/appium/issues/18379)
|
||||
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
|
||||
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [9.3.3](https://github.com/appium/appium/compare/@appium/base-driver@9.3.2...@appium/base-driver@9.3.3) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -11,19 +11,24 @@ const PREFIXED_APPIUM_OPTS_CAP = `${APPIUM_VENDOR_PREFIX}options`;
|
||||
|
||||
/**
|
||||
* Takes primary caps object and merges it into a secondary caps object.
|
||||
* @template {Constraints} [T={}]
|
||||
* @template {Constraints} [U={}]
|
||||
* @param {Capabilities<T>} [primary]
|
||||
* @param {Capabilities<U>} [secondary]
|
||||
* @returns {Merge<Capabilities<T>, Capabilities<U>>}
|
||||
* @template {Constraints} T
|
||||
* @template {Constraints} U
|
||||
* @template {Capabilities<T>} Primary
|
||||
* @template {Capabilities<U>} Secondary
|
||||
* @param {Primary} [primary]
|
||||
* @param {Secondary} [secondary]
|
||||
* @returns {MergeExclusive<Primary, Secondary>}
|
||||
* @see https://www.w3.org/TR/webdriver/#dfn-merging-capabilities)
|
||||
*/
|
||||
function mergeCaps(primary = {}, secondary = {}) {
|
||||
let result = /** @type {Merge<Capabilities<T>, Capabilities<U>>} */ ({
|
||||
function mergeCaps(
|
||||
primary = /** @type {Primary} */ ({}),
|
||||
secondary = /** @type {Secondary} */ ({})
|
||||
) {
|
||||
let result = /** @type {MergeExclusive<Primary, Secondary>} */ ({
|
||||
...primary,
|
||||
});
|
||||
|
||||
for (let [name, value] of /** @type {[keyof typeof secondary, any]} */ (_.toPairs(secondary))) {
|
||||
for (let [name, value] of Object.entries(secondary)) {
|
||||
// Overwriting is not allowed. Primary and secondary must have different properties (w3c rule 4.4)
|
||||
if (!_.isUndefined(primary[name])) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
@@ -38,9 +43,9 @@ function mergeCaps(primary = {}, secondary = {}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validates caps against a set of constraints
|
||||
/**
|
||||
* @template {Constraints} [C={}]
|
||||
* Validates caps against a set of constraints
|
||||
* @template {Constraints} C
|
||||
* @param {Capabilities<C>} caps
|
||||
* @param {C} [constraints]
|
||||
* @param {ValidateCapsOpts} [opts]
|
||||
@@ -121,11 +126,11 @@ function isStandardCap(cap) {
|
||||
|
||||
/**
|
||||
* If the 'appium:' prefix was provided and it's a valid capability, strip out the prefix
|
||||
* @template {Constraints} [C={}]
|
||||
* @param {import('@appium/types').NSCapabilities<C>} caps
|
||||
* @template {Constraints} C
|
||||
* @param {NSCapabilities<C>} caps
|
||||
* @see https://www.w3.org/TR/webdriver/#dfn-extension-capabilities
|
||||
* @internal
|
||||
* @returns {import('@appium/types').Capabilities<C>}
|
||||
* @returns {Capabilities<C>}
|
||||
*/
|
||||
function stripAppiumPrefixes(caps) {
|
||||
// split into prefixed and non-prefixed.
|
||||
@@ -135,14 +140,12 @@ function stripAppiumPrefixes(caps) {
|
||||
);
|
||||
|
||||
// initialize this with the k/v pairs of the non-prefixed caps
|
||||
let strippedCaps = /** @type {import('@appium/types').Capabilities<C>} */ (
|
||||
_.pick(caps, nonPrefixedCaps)
|
||||
);
|
||||
let strippedCaps = /** @type {Capabilities<C>} */ (_.pick(caps, nonPrefixedCaps));
|
||||
const badPrefixedCaps = [];
|
||||
|
||||
// Strip out the 'appium:' prefix
|
||||
for (let prefixedCap of prefixedCaps) {
|
||||
const strippedCapName = /** @type {StringKeyOf<import('@appium/types').Capabilities<C>>} */ (
|
||||
const strippedCapName = /** @type {StringKeyOf<Capabilities<C>>} */ (
|
||||
prefixedCap.substring(APPIUM_VENDOR_PREFIX.length)
|
||||
);
|
||||
|
||||
@@ -175,8 +178,8 @@ function stripAppiumPrefixes(caps) {
|
||||
|
||||
/**
|
||||
* Get an array of all the unprefixed caps that are being used in 'alwaysMatch' and all of the 'firstMatch' object
|
||||
* @template {Constraints} [C={}]
|
||||
* @param {import('@appium/types').W3CCapabilities<C>} caps A capabilities object
|
||||
* @template {Constraints} C
|
||||
* @param {W3CCapabilities<C>} caps A capabilities object
|
||||
*/
|
||||
function findNonPrefixedCaps({alwaysMatch = {}, firstMatch = []}) {
|
||||
return _.chain([alwaysMatch, ...firstMatch])
|
||||
@@ -191,13 +194,25 @@ function findNonPrefixedCaps({alwaysMatch = {}, firstMatch = []}) {
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned by {@linkcode parseCaps}
|
||||
* @template {Constraints} C
|
||||
* @typedef ParsedCaps
|
||||
* @property {NSCapabilities<C>[]} allFirstMatchCaps
|
||||
* @property {Capabilities<C>[]} validatedFirstMatchCaps
|
||||
* @property {NSCapabilities<C>} requiredCaps
|
||||
* @property {Capabilities<C>|null} matchedCaps
|
||||
* @property {string[]} validationErrors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse capabilities
|
||||
* @template {Constraints} [C={}]
|
||||
* @param {import('@appium/types').W3CCapabilities<C>} caps
|
||||
* @template {Constraints} C
|
||||
* @param {W3CCapabilities<C>} caps
|
||||
* @param {C} [constraints]
|
||||
* @param {boolean} [shouldValidateCaps]
|
||||
* @see https://www.w3.org/TR/webdriver/#processing-capabilities
|
||||
* @returns {ParsedCaps<C>}
|
||||
*/
|
||||
function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps = true) {
|
||||
// If capabilities request is not an object, return error (#1.1)
|
||||
@@ -210,8 +225,8 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
|
||||
// Let 'requiredCaps' be property named 'alwaysMatch' from capabilities request (#2)
|
||||
// and 'allFirstMatchCaps' be property named 'firstMatch' from capabilities request (#3)
|
||||
let {
|
||||
alwaysMatch: requiredCaps = {}, // If 'requiredCaps' is undefined, set it to an empty JSON object (#2.1)
|
||||
firstMatch: allFirstMatchCaps = [{}], // If 'firstMatch' is undefined set it to a singleton list with one empty object (#3.1)
|
||||
alwaysMatch: requiredCaps = /** @type {NSCapabilities<C>} */ ({}), // If 'requiredCaps' is undefined, set it to an empty JSON object (#2.1)
|
||||
firstMatch: allFirstMatchCaps = /** @type {NSCapabilities<C>[]} */ ([{}]), // If 'firstMatch' is undefined set it to a singleton list with one empty object (#3.1)
|
||||
} = caps;
|
||||
|
||||
// Reject 'firstMatch' argument if it's not an array (#3.2)
|
||||
@@ -241,6 +256,7 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
|
||||
|
||||
// Strip out the 'appium:' prefix from all
|
||||
let strippedRequiredCaps = stripAppiumPrefixes(requiredCaps);
|
||||
/** @type {Capabilities<C>[]} */
|
||||
let strippedAllFirstMatchCaps = allFirstMatchCaps.map(stripAppiumPrefixes);
|
||||
|
||||
// Validate the requiredCaps. But don't validate 'presence' because if that constraint fails on 'alwaysMatch' it could still pass on one of the 'firstMatch' keys
|
||||
@@ -259,24 +275,23 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
|
||||
/** @type {string[]} */
|
||||
let validationErrors = [];
|
||||
let validatedFirstMatchCaps = _.compact(
|
||||
strippedAllFirstMatchCaps.map(
|
||||
/**
|
||||
* @param {import('@appium/types').Capabilities<C>} firstMatchCaps
|
||||
*/
|
||||
(firstMatchCaps) => {
|
||||
try {
|
||||
// Validate firstMatch caps
|
||||
return shouldValidateCaps
|
||||
? validateCaps(firstMatchCaps, filteredConstraints)
|
||||
: firstMatchCaps;
|
||||
} catch (e) {
|
||||
validationErrors.push(e.message);
|
||||
}
|
||||
strippedAllFirstMatchCaps.map((firstMatchCaps) => {
|
||||
try {
|
||||
// Validate firstMatch caps
|
||||
return shouldValidateCaps
|
||||
? validateCaps(firstMatchCaps, filteredConstraints)
|
||||
: firstMatchCaps;
|
||||
} catch (e) {
|
||||
validationErrors.push(e.message);
|
||||
}
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
// Try to merge requiredCaps with first match capabilities, break once it finds its first match (see spec #6)
|
||||
/**
|
||||
* Try to merge requiredCaps with first match capabilities, break once it finds its first match
|
||||
* (see spec #6)
|
||||
* @type {ParsedCaps<C>['matchedCaps']}
|
||||
*/
|
||||
let matchedCaps = null;
|
||||
for (let firstMatchCaps of validatedFirstMatchCaps) {
|
||||
try {
|
||||
@@ -300,13 +315,14 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
|
||||
};
|
||||
}
|
||||
|
||||
// Calls parseCaps and just returns the matchedCaps variable
|
||||
/**
|
||||
* Calls parseCaps and just returns the matchedCaps variable
|
||||
* @template {Constraints} C
|
||||
* @param {import('@appium/types').W3CCapabilities<C>} w3cCaps
|
||||
* @template {W3CCapabilities<C>} W3CCaps
|
||||
* @param {W3CCaps} w3cCaps
|
||||
* @param {C} [constraints]
|
||||
* @param {boolean} [shouldValidateCaps]
|
||||
* @returns {import('@appium/types').Capabilities<C>}
|
||||
* @returns {Capabilities<C>}
|
||||
*/
|
||||
function processCapabilities(
|
||||
w3cCaps,
|
||||
@@ -400,8 +416,8 @@ function promoteAppiumOptionsForObject(obj) {
|
||||
* capability and promoted it to the top level.
|
||||
*
|
||||
* @template {Constraints} C
|
||||
* @param {import('@appium/types').W3CCapabilities<C>} originalCaps
|
||||
* @return {import('@appium/types').W3CCapabilities<C>} the capabilities with 'options' promoted if necessary
|
||||
* @param {W3CCapabilities<C>} originalCaps
|
||||
* @return {W3CCapabilities<C>} the capabilities with 'options' promoted if necessary
|
||||
*/
|
||||
function promoteAppiumOptions(originalCaps) {
|
||||
const result = {};
|
||||
@@ -451,28 +467,26 @@ export {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').NSCapabilities<C, Extra>} NSCapabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T,U
|
||||
* @typedef {import('type-fest').Merge<T,U>} Merge
|
||||
*/
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('type-fest').StringKeyOf<T>} StringKeyOf
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T,U
|
||||
* @typedef {import('type-fest').MergeExclusive<T, U>} MergeExclusive<T,U>
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import {Constraints, IEventCommands} from '@appium/types';
|
||||
import {mixin} from './mixin';
|
||||
import _ from 'lodash';
|
||||
import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends IEventCommands {}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {mixin} from './mixin';
|
||||
import {BaseDriver} from '../driver';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends IExecuteCommands {}
|
||||
}
|
||||
|
||||
@@ -19,7 +20,7 @@ const ExecuteCommands: IExecuteCommands = {
|
||||
async executeMethod<C extends Constraints>(
|
||||
this: BaseDriver<C>,
|
||||
script: string,
|
||||
protoArgs: [StringRecord] | any[]
|
||||
protoArgs: readonly [StringRecord<unknown>] | readonly unknown[]
|
||||
) {
|
||||
const Driver = this.constructor as DriverClass<Driver<C>>;
|
||||
const commandMetadata = {...Driver.executeMethodMap?.[script]};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends IFindCommands {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './event';
|
||||
export * from './find';
|
||||
export * from './log';
|
||||
export * from './session';
|
||||
export * from './settings';
|
||||
export * from './timeout';
|
||||
export * from './execute';
|
||||
import './event';
|
||||
import './find';
|
||||
import './log';
|
||||
import './session';
|
||||
import './settings';
|
||||
import './timeout';
|
||||
import './execute';
|
||||
|
||||
@@ -4,6 +4,7 @@ import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends ILogCommands {}
|
||||
}
|
||||
|
||||
@@ -15,10 +16,7 @@ const LogCommands: ILogCommands = {
|
||||
return Object.keys(this.supportedLogTypes);
|
||||
},
|
||||
|
||||
async getLog<C extends Constraints>(
|
||||
this: Driver<C>,
|
||||
logType: keyof ILogCommands['supportedLogTypes']
|
||||
) {
|
||||
async getLog<C extends Constraints>(this: Driver<C>, logType: string) {
|
||||
this.log.debug(`Retrieving '${String(logType)}' logs`);
|
||||
|
||||
if (!(logType in this.supportedLogTypes)) {
|
||||
|
||||
@@ -2,13 +2,14 @@ import {Constraints} from '@appium/types';
|
||||
import {BaseDriver} from '../driver';
|
||||
|
||||
/**
|
||||
* This function assigns a mixin `T` to the `FakeDriver` class' prototype.
|
||||
* While each mixin has its own interface which is (in isolation) unrelated to `FakeDriver`, the constraint
|
||||
* on this generic type `T` is that it must be a partial of `FakeDriver`'s interface. This enforces
|
||||
* that it does not conflict with the existing interface of `FakeDriver`. In that way, you can
|
||||
* This function assigns a mixin `T` to the `BaseDriver` class' prototype.
|
||||
* While each mixin has its own interface which is (in isolation) unrelated to `BaseDriver`, the constraint
|
||||
* on this generic type `T` is that it must be a partial of `BaseDriver`'s interface. This enforces
|
||||
* that it does not conflict with the existing interface of `BaseDriver`. In that way, you can
|
||||
* think of it as a type guard.
|
||||
* @param mixin Mixin implementation
|
||||
*/
|
||||
export function mixin<C extends Constraints, T extends Partial<BaseDriver<C>>>(mixin: T): void {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.assign(BaseDriver.prototype, mixin);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {Constraints, ISessionCommands, MultiSessionData} from '@appium/types';
|
||||
import {Constraints, ISessionCommands, MultiSessionData, SingularSessionData} from '@appium/types';
|
||||
import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
interface BaseDriver<C extends Constraints> extends ISessionCommands {}
|
||||
interface BaseDriver<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
C extends Constraints
|
||||
> extends ISessionCommands {}
|
||||
}
|
||||
|
||||
const SessionCommands: ISessionCommands = {
|
||||
async getSessions<C extends Constraints>(this: BaseDriver<C>) {
|
||||
const ret: MultiSessionData[] = [];
|
||||
const ret: MultiSessionData<C>[] = [];
|
||||
|
||||
if (this.sessionId) {
|
||||
ret.push({
|
||||
@@ -24,10 +27,9 @@ const SessionCommands: ISessionCommands = {
|
||||
* Returns capabilities for the session and event history (if applicable)
|
||||
*/
|
||||
async getSession<C extends Constraints>(this: BaseDriver<C>) {
|
||||
if (this.caps.eventTimings) {
|
||||
return {...this.caps, events: this.eventHistory};
|
||||
}
|
||||
return this.caps;
|
||||
return <SingularSessionData<C>>(
|
||||
(this.caps.eventTimings ? {...this.caps, events: this.eventHistory} : this.caps)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Constraints, ISettingsCommands, StringRecord} from '@appium/types';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends ISettingsCommands {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable require-await */
|
||||
import {waitForCondition} from 'asyncbox';
|
||||
import _ from 'lodash';
|
||||
import {util} from '@appium/support';
|
||||
@@ -11,13 +7,14 @@ import {Constraints, ITimeoutCommands} from '@appium/types';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface BaseDriver<C extends Constraints> extends ITimeoutCommands {}
|
||||
}
|
||||
|
||||
const MIN_TIMEOUT = 0;
|
||||
|
||||
const TimeoutCommands: ITimeoutCommands = {
|
||||
async timeouts(type, ms, script, pageLoad, implicit) {
|
||||
async timeouts<C extends Constraints>(this: BaseDriver<C>, type, ms, script, pageLoad, implicit) {
|
||||
if (util.hasValue(type) && util.hasValue(ms)) {
|
||||
this.log.debug(`MJSONWP timeout arguments: ${JSON.stringify({type, ms})}}`);
|
||||
|
||||
@@ -66,42 +63,42 @@ const TimeoutCommands: ITimeoutCommands = {
|
||||
},
|
||||
|
||||
// implicit
|
||||
async implicitWaitW3C(ms) {
|
||||
async implicitWaitW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
await this.implicitWait(ms);
|
||||
},
|
||||
|
||||
async implicitWaitMJSONWP(ms) {
|
||||
async implicitWaitMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
await this.implicitWait(ms);
|
||||
},
|
||||
|
||||
async implicitWait(ms) {
|
||||
await this.setImplicitWait(this.parseTimeoutArgument(ms));
|
||||
async implicitWait<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
this.setImplicitWait(this.parseTimeoutArgument(ms));
|
||||
},
|
||||
|
||||
// pageLoad
|
||||
async pageLoadTimeoutW3C(ms) {
|
||||
async pageLoadTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
|
||||
},
|
||||
|
||||
async pageLoadTimeoutMJSONWP(ms) {
|
||||
async pageLoadTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
|
||||
},
|
||||
|
||||
// script
|
||||
async scriptTimeoutW3C(ms) {
|
||||
async scriptTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for script.');
|
||||
},
|
||||
|
||||
async scriptTimeoutMJSONWP(ms) {
|
||||
async scriptTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for script.');
|
||||
},
|
||||
|
||||
// command
|
||||
async newCommandTimeout(ms) {
|
||||
async newCommandTimeout<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
this.setNewCommandTimeout(this.parseTimeoutArgument(ms));
|
||||
},
|
||||
|
||||
setImplicitWait(ms) {
|
||||
setImplicitWait<C extends Constraints>(this: BaseDriver<C>, ms: number) {
|
||||
// eslint-disable-line require-await
|
||||
this.implicitWaitMs = ms;
|
||||
this.log.debug(`Set implicit wait to ${ms}ms`);
|
||||
@@ -115,7 +112,7 @@ const TimeoutCommands: ITimeoutCommands = {
|
||||
}
|
||||
},
|
||||
|
||||
setNewCommandTimeout(ms) {
|
||||
setNewCommandTimeout<C extends Constraints>(this: BaseDriver<C>, ms: number) {
|
||||
this.newCommandTimeoutMs = ms;
|
||||
this.log.debug(`Set new command timeout to ${ms}ms`);
|
||||
if (this.managedDrivers && this.managedDrivers.length) {
|
||||
@@ -128,7 +125,10 @@ const TimeoutCommands: ITimeoutCommands = {
|
||||
}
|
||||
},
|
||||
|
||||
async implicitWaitForCondition(condFn) {
|
||||
async implicitWaitForCondition<C extends Constraints>(
|
||||
this: BaseDriver<C>,
|
||||
condFn: (...args: any[]) => Promise<any>
|
||||
) {
|
||||
this.log.debug(`Waiting up to ${this.implicitWaitMs} ms for condition`);
|
||||
const wrappedCondFn = async (...args: any[]) => {
|
||||
// reset command timeout
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import {fs, logger} from '@appium/support';
|
||||
import {logger} from '@appium/support';
|
||||
import AsyncLock from 'async-lock';
|
||||
import {EventEmitter} from 'events';
|
||||
import _ from 'lodash';
|
||||
@@ -9,18 +9,16 @@ import os from 'os';
|
||||
import {DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
||||
import {errors} from '../protocol';
|
||||
import DeviceSettings from './device-settings';
|
||||
import helpers from './helpers';
|
||||
|
||||
// for compat with running tests transpiled and in-place
|
||||
const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
|
||||
import helpers, {BASEDRIVER_VER} from './helpers';
|
||||
|
||||
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
|
||||
|
||||
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @implements {Core<C>}
|
||||
* @template {Constraints} C
|
||||
* @template {StringRecord} [Settings=StringRecord]
|
||||
* @implements {Core<C, Settings>}
|
||||
*/
|
||||
class DriverCore {
|
||||
/**
|
||||
@@ -40,7 +38,7 @@ class DriverCore {
|
||||
opts;
|
||||
|
||||
/**
|
||||
* @type {ServerArgs}
|
||||
* @type {import('@appium/types').DriverOpts<C>}
|
||||
*/
|
||||
initialOpts;
|
||||
|
||||
@@ -98,7 +96,6 @@ class DriverCore {
|
||||
_log;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
shutdownUnexpectedly;
|
||||
@@ -119,7 +116,7 @@ class DriverCore {
|
||||
* we set it to an empty DeviceSettings instance here to make sure that the
|
||||
* default settings are applied even if an extending driver doesn't utilize
|
||||
* the settings functionality itself
|
||||
* @type {DeviceSettings}
|
||||
* @type {DeviceSettings<Settings>}
|
||||
*/
|
||||
settings;
|
||||
|
||||
@@ -184,11 +181,9 @@ class DriverCore {
|
||||
* specific driver sessions. This data can be later used to adjust
|
||||
* properties for driver instances running in parallel.
|
||||
* Override it in inherited driver classes if necessary.
|
||||
*
|
||||
* @return {Record<string,unknown>} Driver properties mapping
|
||||
*/
|
||||
get driverData() {
|
||||
return {};
|
||||
return /** @type {import('@appium/types').DriverData} */ ({});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +255,7 @@ class DriverCore {
|
||||
* @returns {Core<C> | null}
|
||||
*/
|
||||
driverForSession(sessionId) {
|
||||
return this;
|
||||
return /** @type {Core<C> | null} */ (this);
|
||||
}
|
||||
|
||||
isMjsonwpProtocol() {
|
||||
@@ -448,19 +443,10 @@ export {DriverCore};
|
||||
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
|
||||
*/
|
||||
/**
|
||||
* @template {StringRecord} [T={}]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<T>} W3CCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Core<C>} Core
|
||||
* @template {StringRecord} [S=StringRecord]
|
||||
* @typedef {import('@appium/types').Core<C, S>} Core
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,13 +9,13 @@ import {errors} from '../protocol/errors';
|
||||
export const MAX_SETTINGS_SIZE = 20 * 1024 * 1024; // 20 MB
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {import('@appium/types').StringRecord} T
|
||||
* @implements {IDeviceSettings<T>}
|
||||
*/
|
||||
class DeviceSettings {
|
||||
export class DeviceSettings {
|
||||
/**
|
||||
* @protected
|
||||
* @type {Record<string,T>}
|
||||
* @type {T}
|
||||
*/
|
||||
_settings;
|
||||
|
||||
@@ -27,17 +27,17 @@ class DeviceSettings {
|
||||
|
||||
/**
|
||||
* Creates a _shallow copy_ of the `defaultSettings` parameter!
|
||||
* @param {Record<string,T>} [defaultSettings]
|
||||
* @param {T} [defaultSettings]
|
||||
* @param {import('@appium/types').SettingsUpdateListener<T>} [onSettingsUpdate]
|
||||
*/
|
||||
constructor(defaultSettings = {}, onSettingsUpdate = async () => {}) {
|
||||
constructor(defaultSettings = /** @type {T} */ ({}), onSettingsUpdate = async () => {}) {
|
||||
this._settings = {...defaultSettings};
|
||||
this._onSettingsUpdate = onSettingsUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* calls updateSettings from implementing driver every time a setting is changed.
|
||||
* @param {Record<string,T>} newSettings
|
||||
* @param {T} newSettings
|
||||
*/
|
||||
async update(newSettings) {
|
||||
if (!_.isPlainObject(newSettings)) {
|
||||
@@ -54,8 +54,7 @@ class DeviceSettings {
|
||||
);
|
||||
}
|
||||
|
||||
const props = /** @type {(keyof T & string)[]} */ (_.keys(newSettings));
|
||||
for (const prop of props) {
|
||||
for (const prop in newSettings) {
|
||||
if (!_.isUndefined(this._settings[prop])) {
|
||||
if (this._settings[prop] === newSettings[prop]) {
|
||||
log.debug(`The value of '${prop}' setting did not change. Skipping the update for it`);
|
||||
@@ -73,9 +72,8 @@ class DeviceSettings {
|
||||
}
|
||||
|
||||
export default DeviceSettings;
|
||||
export {DeviceSettings};
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('@appium/types').DeviceSettings<T>} IDeviceSettings
|
||||
* @template {import('@appium/types').StringRecord} [T=import('@appium/types').StringRecord]
|
||||
* @typedef {import('@appium/types').IDeviceSettings<T>} IDeviceSettings
|
||||
*/
|
||||
|
||||
+67
-173
@@ -1,6 +1,3 @@
|
||||
/* eslint-disable require-await */
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import {validateCaps, processCapabilities} from './capabilities';
|
||||
import {DriverCore} from './core';
|
||||
import {util} from '@appium/support';
|
||||
@@ -9,7 +6,21 @@ import _ from 'lodash';
|
||||
import {fixCaps, isW3cCaps} from '../helpers/capabilities';
|
||||
import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
|
||||
import helpers from './helpers';
|
||||
import {BASE_DESIRED_CAP_CONSTRAINTS} from '@appium/types';
|
||||
import {
|
||||
AppiumServer,
|
||||
BaseDriverCapConstraints,
|
||||
BASE_DESIRED_CAP_CONSTRAINTS,
|
||||
Capabilities,
|
||||
Constraints,
|
||||
DefaultCreateSessionResult,
|
||||
Driver,
|
||||
DriverCaps,
|
||||
DriverData,
|
||||
DriverOpts,
|
||||
ServerArgs,
|
||||
StringRecord,
|
||||
W3CDriverCaps,
|
||||
} from '@appium/types';
|
||||
|
||||
const EVENT_SESSION_INIT = 'newSessionRequested';
|
||||
const EVENT_SESSION_START = 'newSessionStarted';
|
||||
@@ -17,56 +28,31 @@ const EVENT_SESSION_QUIT_START = 'quitSessionRequested';
|
||||
const EVENT_SESSION_QUIT_DONE = 'quitSessionFinished';
|
||||
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
|
||||
|
||||
/**
|
||||
* @implements {SessionHandler<C>}
|
||||
* @template {Constraints} C
|
||||
* @template {StringRecord} [CArgs=StringRecord]
|
||||
* @implements {Driver<C, CArgs>}
|
||||
* @extends {DriverCore<C>}
|
||||
*/
|
||||
export class BaseDriver extends DriverCore {
|
||||
/**
|
||||
* @type {CArgs & ServerArgs}
|
||||
*/
|
||||
cliArgs;
|
||||
export class BaseDriver<
|
||||
C extends Constraints,
|
||||
CArgs extends StringRecord = StringRecord,
|
||||
Settings extends StringRecord = StringRecord
|
||||
>
|
||||
extends DriverCore<C, Settings>
|
||||
implements Driver<C, CArgs>
|
||||
{
|
||||
cliArgs: CArgs & ServerArgs;
|
||||
|
||||
/**
|
||||
* @type {Capabilities<C>}
|
||||
*/
|
||||
caps;
|
||||
caps: DriverCaps<C>;
|
||||
originalCaps: W3CDriverCaps<C>;
|
||||
desiredCapConstraints: C;
|
||||
opts: DriverOpts<C>;
|
||||
server?: AppiumServer;
|
||||
serverHost?: string;
|
||||
serverPort?: number;
|
||||
serverPath?: string;
|
||||
|
||||
/**
|
||||
* @type {W3CCapabilities<C>}
|
||||
*/
|
||||
originalCaps;
|
||||
|
||||
/**
|
||||
* @type {C}
|
||||
*/
|
||||
desiredCapConstraints;
|
||||
|
||||
/**
|
||||
* @type {DriverOpts<C> & DriverOpts<BaseDriverCapConstraints>}
|
||||
*/
|
||||
opts;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DriverOpts<C>} opts
|
||||
* @param {boolean} shouldValidateCaps
|
||||
*/
|
||||
constructor(opts = /** @type {DriverOpts<C>} */ ({}), shouldValidateCaps = true) {
|
||||
constructor(opts: DriverOpts<C>, shouldValidateCaps = true) {
|
||||
super(opts, shouldValidateCaps);
|
||||
|
||||
/**
|
||||
* This must be assigned here because the declaration of {@linkcode BaseDriver.opts} above
|
||||
* blows away {@linkcode DriverCore.opts}.
|
||||
*/
|
||||
this.opts = opts;
|
||||
this.caps = {} as DriverCaps<C>;
|
||||
|
||||
this.caps = {};
|
||||
|
||||
this.cliArgs = /** @type {CArgs & ServerArgs} */ ({});
|
||||
this.cliArgs = {} as CArgs & ServerArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,25 +61,20 @@ export class BaseDriver extends DriverCore {
|
||||
* Subclasses _shouldn't_ need to use this. If you need to use this, please create
|
||||
* an issue:
|
||||
* @see https://github.com/appium/appium/issues/new
|
||||
* @type {Readonly<BaseDriverCapConstraints & C>}
|
||||
* @protected
|
||||
*/
|
||||
get _desiredCapConstraints() {
|
||||
protected get _desiredCapConstraints(): Readonly<BaseDriverCapConstraints & C> {
|
||||
return Object.freeze(_.merge({}, BASE_DESIRED_CAP_CONSTRAINTS, this.desiredCapConstraints));
|
||||
}
|
||||
|
||||
// This is the main command handler for the driver. It wraps command
|
||||
// execution with timeout logic, checking that we have a valid session,
|
||||
// and ensuring that we execute commands one at a time. This method is called
|
||||
// by MJSONWP's express router.
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {...any} args
|
||||
* @returns {Promise<any>}
|
||||
* This is the main command handler for the driver. It wraps command
|
||||
* execution with timeout logic, checking that we have a valid session,
|
||||
* and ensuring that we execute commands one at a time. This method is called
|
||||
* by MJSONWP's express router.
|
||||
*/
|
||||
async executeCommand(cmd, ...args) {
|
||||
async executeCommand<T = unknown>(cmd: string, ...args: any[]): Promise<T> {
|
||||
// get start time for this command, and log in special cases
|
||||
let startTime = Date.now();
|
||||
const startTime = Date.now();
|
||||
|
||||
if (cmd === 'createSession') {
|
||||
// If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
|
||||
@@ -161,12 +142,8 @@ export class BaseDriver extends DriverCore {
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Error|import('../protocol/errors').NoSuchDriverError} err
|
||||
*/
|
||||
async startUnexpectedShutdown(
|
||||
err = new errors.NoSuchDriverError('The driver was unexpectedly shut down!')
|
||||
err: Error = new errors.NoSuchDriverError('The driver was unexpectedly shut down!')
|
||||
) {
|
||||
this.eventEmitter.emit(ON_UNEXPECTED_SHUTDOWN_EVENT, err); // allow others to listen for this
|
||||
this.shutdownUnexpectedly = true;
|
||||
@@ -200,14 +177,7 @@ export class BaseDriver extends DriverCore {
|
||||
}, this.newCommandTimeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('@appium/types').AppiumServer} server
|
||||
* @param {string} host
|
||||
* @param {number} port
|
||||
* @param {string} path
|
||||
*/
|
||||
assignServer(server, host, port, path) {
|
||||
assignServer(server: AppiumServer, host: string, port: number, path: string) {
|
||||
this.server = server;
|
||||
this.serverHost = host;
|
||||
this.serverPort = port;
|
||||
@@ -223,8 +193,8 @@ export class BaseDriver extends DriverCore {
|
||||
this.log.debug('Running generic full reset');
|
||||
|
||||
// preserving state
|
||||
let currentConfig = {};
|
||||
for (let property of [
|
||||
const currentConfig = {};
|
||||
for (const property of [
|
||||
'implicitWaitMs',
|
||||
'newCommandTimeoutMs',
|
||||
'sessionId',
|
||||
@@ -233,9 +203,6 @@ export class BaseDriver extends DriverCore {
|
||||
currentConfig[property] = this[property];
|
||||
}
|
||||
|
||||
// We also need to preserve the unexpected shutdown, and make sure it is not cancelled during reset.
|
||||
this.resetOnUnexpectedShutdown = () => {};
|
||||
|
||||
try {
|
||||
if (this.sessionId !== null) {
|
||||
await this.deleteSession(this.sessionId);
|
||||
@@ -244,7 +211,7 @@ export class BaseDriver extends DriverCore {
|
||||
await this.createSession(this.originalCaps);
|
||||
} finally {
|
||||
// always restore state.
|
||||
for (let [key, value] of _.toPairs(currentConfig)) {
|
||||
for (const [key, value] of _.toPairs(currentConfig)) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -257,13 +224,14 @@ export class BaseDriver extends DriverCore {
|
||||
* Appium 2 has dropped the support of these, so now we only accept capability
|
||||
* objects in W3C format and thus allow any of the three arguments to represent
|
||||
* the latter.
|
||||
* @param {W3CCapabilities<C>} w3cCapabilities1
|
||||
* @param {W3CCapabilities<C>} [w3cCapabilities2]
|
||||
* @param {W3CCapabilities<C>} [w3cCapabilities]
|
||||
* @param {DriverData[]} [driverData]
|
||||
* @returns {Promise<[string,Capabilities<C>]>}
|
||||
*/
|
||||
async createSession(w3cCapabilities1, w3cCapabilities2, w3cCapabilities, driverData) {
|
||||
async createSession(
|
||||
w3cCapabilities1: W3CDriverCaps<C>,
|
||||
w3cCapabilities2?: W3CDriverCaps<C>,
|
||||
w3cCapabilities?: W3CDriverCaps<C>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
driverData?: DriverData[]
|
||||
): Promise<DefaultCreateSessionResult<C>> {
|
||||
if (this.sessionId !== null) {
|
||||
throw new errors.SessionNotCreatedError(
|
||||
'Cannot create a new session while one is in progress'
|
||||
@@ -289,15 +257,14 @@ export class BaseDriver extends DriverCore {
|
||||
`Creating session with W3C capabilities: ${JSON.stringify(originalCaps, null, 2)}`
|
||||
);
|
||||
|
||||
/** @type {Capabilities<C>} */
|
||||
let caps;
|
||||
let caps: DriverCaps<C>;
|
||||
try {
|
||||
caps = processCapabilities(
|
||||
originalCaps,
|
||||
this._desiredCapConstraints,
|
||||
this.shouldValidateCaps
|
||||
);
|
||||
caps = fixCaps(caps, this._desiredCapConstraints, this.log);
|
||||
) as DriverCaps<C>;
|
||||
caps = fixCaps(caps, this._desiredCapConstraints, this.log) as DriverCaps<C>;
|
||||
} catch (e) {
|
||||
throw new errors.SessionNotCreatedError(e.message);
|
||||
}
|
||||
@@ -334,7 +301,7 @@ export class BaseDriver extends DriverCore {
|
||||
}
|
||||
|
||||
if (!_.isUndefined(this.caps.newCommandTimeout)) {
|
||||
this.newCommandTimeoutMs = /** @type {number} */ (this.caps.newCommandTimeout) * 1000;
|
||||
this.newCommandTimeoutMs = (this.caps.newCommandTimeout as number) * 1000;
|
||||
}
|
||||
|
||||
this._log.prefix = helpers.generateDriverLogPrefix(this, this.sessionId);
|
||||
@@ -344,32 +311,23 @@ export class BaseDriver extends DriverCore {
|
||||
return [this.sessionId, caps];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [sessionId]
|
||||
* @param {DriverData[]} [driverData]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteSession(sessionId, driverData) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async deleteSession(sessionId?: string | null) {
|
||||
await this.clearNewCommandTimeout();
|
||||
if (this.isCommandsQueueEnabled && this.commandsQueueGuard.isBusy()) {
|
||||
// simple hack to release pending commands if they exist
|
||||
// @ts-ignore
|
||||
for (const key of _.keys(this.commandsQueueGuard.queues)) {
|
||||
// @ts-ignore
|
||||
this.commandsQueueGuard.queues[key] = [];
|
||||
// @ts-expect-error private API
|
||||
const queues = this.commandsQueueGuard.queues;
|
||||
for (const key of _.keys(queues)) {
|
||||
queues[key] = [];
|
||||
}
|
||||
}
|
||||
this.sessionId = null;
|
||||
this._log.prefix = helpers.generateDriverLogPrefix(this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Capabilities<C>} caps
|
||||
*/
|
||||
logExtraCaps(caps) {
|
||||
let extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
|
||||
logExtraCaps(caps: Capabilities<C>) {
|
||||
const extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
|
||||
if (extraCaps.length) {
|
||||
this.log.warn(
|
||||
`The following capabilities were provided, but are not ` + `recognized by Appium:`
|
||||
@@ -380,12 +338,7 @@ export class BaseDriver extends DriverCore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Capabilities<C>} caps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateDesiredCaps(caps) {
|
||||
validateDesiredCaps(caps: any): caps is DriverCaps<C> {
|
||||
if (!this.shouldValidateCaps) {
|
||||
return true;
|
||||
}
|
||||
@@ -407,65 +360,6 @@ export class BaseDriver extends DriverCore {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
export * from './commands';
|
||||
|
||||
export default BaseDriver;
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').HTTPMethod} HTTPMethod
|
||||
* @typedef {import('@appium/types').DriverData} DriverData
|
||||
* @typedef {import('@appium/types').Constraints} Constraints
|
||||
* @typedef {import('@appium/types').Constraint} Constraint
|
||||
* @typedef {import('@appium/types').StringRecord} StringRecord
|
||||
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
|
||||
* @typedef {import('@appium/types').ServerArgs} ServerArgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback UpdateServerCallback
|
||||
* @param {import('express').Express} app - Express app
|
||||
* @param {import('@appium/types').AppiumServer} httpServer - HTTP server
|
||||
* @returns {import('type-fest').Promisable<void>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is used to extend {@linkcode BaseDriver} by the mixins and also external drivers.
|
||||
* @template {Constraints} C
|
||||
* @template [Proto={}]
|
||||
* @template [Static={}]
|
||||
* @typedef {import('@appium/types').Class<BaseDriver<C> & Proto,import('@appium/types').DriverStatic & Static>} BaseDriverBase
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @typedef {import('@appium/types').SessionHandler<[string, object],void, C>} SessionHandler
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord} [CArgs=StringRecord]
|
||||
* @typedef {import('@appium/types').Driver<C, CArgs>} Driver
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').ExternalDriver<C>} ExternalDriver
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').DriverOpts<C>} DriverOpts
|
||||
*/
|
||||
@@ -2,16 +2,24 @@ import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import logger from './logger';
|
||||
import {tempDir, fs, util, zip, net, timing, node} from '@appium/support';
|
||||
import {tempDir, fs, util, zip, timing, node} from '@appium/support';
|
||||
import LRU from 'lru-cache';
|
||||
import AsyncLock from 'async-lock';
|
||||
import axios from 'axios';
|
||||
import B from 'bluebird';
|
||||
|
||||
// for compat with running tests transpiled and in-place
|
||||
const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
|
||||
const IPA_EXT = '.ipa';
|
||||
const ZIP_EXTS = ['.zip', IPA_EXT];
|
||||
const ZIP_EXTS = new Set(['.zip', IPA_EXT]);
|
||||
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
|
||||
const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
|
||||
const MAX_CACHED_APPS = 1024;
|
||||
const HTTP_STATUS_NOT_MODIFIED = 304;
|
||||
const DEFAULT_REQ_HEADERS = Object.freeze({
|
||||
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
|
||||
});
|
||||
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
||||
const APPLICATIONS_CACHE = new LRU({
|
||||
max: MAX_CACHED_APPS,
|
||||
ttl: CACHED_APPS_MAX_AGE, // expire after 24 hours
|
||||
@@ -52,77 +60,6 @@ process.on('exit', () => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {Promise<import('axios').AxiosResponse['headers']>}
|
||||
*/
|
||||
async function retrieveHeaders(url) {
|
||||
try {
|
||||
return (
|
||||
await axios({
|
||||
url,
|
||||
method: 'HEAD',
|
||||
timeout: 5000,
|
||||
})
|
||||
).headers;
|
||||
} catch (e) {
|
||||
logger.info(`Cannot send HEAD request to '${url}'. Original error: ${e.message}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getCachedApplicationPath(link, currentAppProps = {}, cachedAppInfo = {}) {
|
||||
const refresh = () => {
|
||||
logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!_.isPlainObject(cachedAppInfo) || !_.isPlainObject(currentAppProps)) {
|
||||
// if an invalid arg is passed then assume cache miss
|
||||
return refresh();
|
||||
}
|
||||
|
||||
const {
|
||||
lastModified: currentModified,
|
||||
immutable: currentImmutable,
|
||||
// maxAge is in seconds
|
||||
maxAge: currentMaxAge,
|
||||
} = currentAppProps;
|
||||
const {
|
||||
// Date instance
|
||||
lastModified,
|
||||
// boolean
|
||||
immutable,
|
||||
// Unix time in milliseconds
|
||||
timestamp,
|
||||
fullPath,
|
||||
} = cachedAppInfo;
|
||||
if (lastModified && currentModified) {
|
||||
if (currentModified.getTime() <= lastModified.getTime()) {
|
||||
logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
|
||||
return fullPath;
|
||||
}
|
||||
logger.debug(`The application at ${link} has been modified since ${lastModified}`);
|
||||
return refresh();
|
||||
}
|
||||
if (immutable && currentImmutable) {
|
||||
logger.debug(`The application at ${link} is immutable`);
|
||||
return fullPath;
|
||||
}
|
||||
if (currentMaxAge && timestamp) {
|
||||
const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
|
||||
if (msLeft > 0) {
|
||||
logger.debug(
|
||||
`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`
|
||||
);
|
||||
return fullPath;
|
||||
}
|
||||
logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
|
||||
}
|
||||
return refresh();
|
||||
}
|
||||
|
||||
function verifyAppExtension(app, supportedAppExtensions) {
|
||||
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
|
||||
return app;
|
||||
@@ -160,56 +97,14 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef PostProcessOptions
|
||||
* @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
|
||||
* - packageHash: SHA1 hash of the package if it is a file and not a folder
|
||||
* - lastModified: Optional Date instance, the value of file's `Last-Modified` header
|
||||
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
|
||||
* in `Cache-control` header
|
||||
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
|
||||
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
|
||||
* milliseconds)
|
||||
* - integrity: An object containing either `file` property with SHA1 hash of the file
|
||||
* or `folder` property with total amount of cached files and subfolders
|
||||
* - fullPath: the full path to the cached app
|
||||
* @property {boolean} isUrl Whether the app has been downloaded from a remote URL
|
||||
* @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
|
||||
* responds to HEAD requests. All header names are normalized to lowercase.
|
||||
* @property {string} appPath A string containing full path to the preprocessed application package (either
|
||||
* downloaded or a local one)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PostProcessResult
|
||||
* @property {string} appPath The full past to the post-processed application package on the
|
||||
* local file system (might be a file or a folder path)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ConfigureAppOptions
|
||||
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} [onPostProcess]
|
||||
* Optional function, which should be applied
|
||||
* to the application after it is downloaded/preprocessed. This function may be async
|
||||
* and is expected to accept single object parameter.
|
||||
* The function is expected to either return a falsy value, which means the app must not be
|
||||
* cached and a fresh copy of it is downloaded each time. If this function returns an object
|
||||
* containing `appPath` property then the integrity of it will be verified and stored into
|
||||
* the cache.
|
||||
* @property {string[]} supportedExtensions List of supported application extensions (
|
||||
* including starting dots). This property is mandatory and must not be empty.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prepares an app to be used in an automated test. The app gets cached automatically
|
||||
* if it is an archive or if it is downloaded from an URL.
|
||||
* If the downloaded app has `.zip` extension, this method will unzip it.
|
||||
* The unzip does not work when `onPostProcess` is provided.
|
||||
*
|
||||
* @param {string} app Either a full path to the app or a remote URL
|
||||
* @param {string|string[]|ConfigureAppOptions} options
|
||||
* @returns The full path to the resulting application bundle
|
||||
* @param {string} app
|
||||
* @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
|
||||
*/
|
||||
async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({})) {
|
||||
async function configureApp(
|
||||
app,
|
||||
options = /** @type {import('@appium/types').ConfigureAppOptions} */ ({})
|
||||
) {
|
||||
if (!_.isString(app)) {
|
||||
// immediately shortcircuit if not given an app
|
||||
return;
|
||||
@@ -233,13 +128,14 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
||||
let newApp = app;
|
||||
let shouldUnzipApp = false;
|
||||
let packageHash = null;
|
||||
/** @type {import('axios').AxiosResponse['headers']?} */
|
||||
let headers = null;
|
||||
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
||||
let headers = undefined;
|
||||
/** @type {RemoteAppProps} */
|
||||
const remoteAppProps = {
|
||||
lastModified: null,
|
||||
immutable: false,
|
||||
maxAge: null,
|
||||
etag: null,
|
||||
};
|
||||
const {protocol, pathname} = url.parse(newApp);
|
||||
const isUrl = protocol === null ? false : ['http:', 'https:'].includes(protocol);
|
||||
@@ -250,94 +146,121 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
||||
if (isUrl) {
|
||||
// Use the app from remote URL
|
||||
logger.info(`Using downloadable app '${newApp}'`);
|
||||
headers = await retrieveHeaders(newApp);
|
||||
if (!_.isEmpty(headers)) {
|
||||
if (headers['last-modified']) {
|
||||
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
||||
}
|
||||
logger.debug(`Last-Modified: ${headers['last-modified']}`);
|
||||
if (headers['cache-control']) {
|
||||
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
|
||||
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
|
||||
if (maxAgeMatch) {
|
||||
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
||||
}
|
||||
}
|
||||
logger.debug(`Cache-Control: ${headers['cache-control']}`);
|
||||
}
|
||||
const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
|
||||
if (cachedPath) {
|
||||
if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
|
||||
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
||||
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
||||
}
|
||||
logger.info(
|
||||
`The application at '${cachedPath}' does not exist anymore ` +
|
||||
`or its integrity has been damaged. Deleting it from the internal cache`
|
||||
);
|
||||
APPLICATIONS_CACHE.delete(app);
|
||||
const reqHeaders = {
|
||||
...DEFAULT_REQ_HEADERS,
|
||||
};
|
||||
if (cachedAppInfo?.etag) {
|
||||
reqHeaders['if-none-match'] = remoteAppProps.etag;
|
||||
} else if (cachedAppInfo?.lastModified) {
|
||||
reqHeaders['if-modified-since'] = remoteAppProps.lastModified?.toString();
|
||||
}
|
||||
|
||||
let fileName = null;
|
||||
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
||||
replacement: SANITIZE_REPLACEMENT,
|
||||
});
|
||||
const extname = path.extname(basename);
|
||||
// to determine if we need to unzip the app, we have a number of places
|
||||
// to look: content type, content disposition, or the file extension
|
||||
if (ZIP_EXTS.includes(extname)) {
|
||||
fileName = basename;
|
||||
shouldUnzipApp = true;
|
||||
}
|
||||
if (headers['content-type']) {
|
||||
const ct = headers['content-type'];
|
||||
logger.debug(`Content-Type: ${ct}`);
|
||||
// the filetype may not be obvious for certain urls, so check the mime type too
|
||||
if (
|
||||
ZIP_MIME_TYPES.some((mimeType) =>
|
||||
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
|
||||
)
|
||||
) {
|
||||
if (!fileName) {
|
||||
fileName = `${DEFAULT_BASENAME}.zip`;
|
||||
let {headers, stream, status} = await queryAppLink(newApp, reqHeaders);
|
||||
try {
|
||||
if (!_.isEmpty(headers)) {
|
||||
logger.debug(`Etag: ${remoteAppProps?.etag} -> ${headers.etag}`);
|
||||
if (headers.etag) {
|
||||
remoteAppProps.etag = headers.etag;
|
||||
}
|
||||
logger.debug(
|
||||
`Last-Modified: ${remoteAppProps?.['last-modified']} -> ${headers['last-modified']}`
|
||||
);
|
||||
if (headers['last-modified']) {
|
||||
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
||||
}
|
||||
logger.debug(
|
||||
`Cache-Control: ${remoteAppProps?.['cache-control']} -> ${headers['cache-control']}`
|
||||
);
|
||||
if (headers['cache-control']) {
|
||||
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
|
||||
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
|
||||
if (maxAgeMatch) {
|
||||
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
|
||||
if (await isAppIntegrityOk(cachedAppInfo.fullPath, cachedAppInfo.integrity)) {
|
||||
logger.info(`Reusing previously downloaded application at '${cachedAppInfo.fullPath}'`);
|
||||
return verifyAppExtension(cachedAppInfo.fullPath, supportedAppExtensions);
|
||||
}
|
||||
logger.info(
|
||||
`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
||||
`or its integrity has been damaged. Deleting it from the internal cache`
|
||||
);
|
||||
APPLICATIONS_CACHE.delete(app);
|
||||
|
||||
if (!stream.closed) {
|
||||
stream.destroy();
|
||||
}
|
||||
({stream, headers, status} = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS}));
|
||||
}
|
||||
|
||||
let fileName = null;
|
||||
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
|
||||
replacement: SANITIZE_REPLACEMENT,
|
||||
});
|
||||
const extname = path.extname(basename);
|
||||
// to determine if we need to unzip the app, we have a number of places
|
||||
// to look: content type, content disposition, or the file extension
|
||||
if (ZIP_EXTS.has(extname)) {
|
||||
fileName = basename;
|
||||
shouldUnzipApp = true;
|
||||
}
|
||||
}
|
||||
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
|
||||
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
||||
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
|
||||
if (match) {
|
||||
fileName = fs.sanitizeName(match[1], {
|
||||
replacement: SANITIZE_REPLACEMENT,
|
||||
});
|
||||
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.includes(path.extname(fileName));
|
||||
if (headers['content-type']) {
|
||||
const ct = headers['content-type'];
|
||||
logger.debug(`Content-Type: ${ct}`);
|
||||
// the filetype may not be obvious for certain urls, so check the mime type too
|
||||
if (
|
||||
ZIP_MIME_TYPES.some((mimeType) =>
|
||||
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
|
||||
)
|
||||
) {
|
||||
if (!fileName) {
|
||||
fileName = `${DEFAULT_BASENAME}.zip`;
|
||||
}
|
||||
shouldUnzipApp = true;
|
||||
}
|
||||
}
|
||||
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
|
||||
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
||||
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
|
||||
if (match) {
|
||||
fileName = fs.sanitizeName(match[1], {
|
||||
replacement: SANITIZE_REPLACEMENT,
|
||||
});
|
||||
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.has(path.extname(fileName));
|
||||
}
|
||||
}
|
||||
if (!fileName) {
|
||||
// assign the default file name and the extension if none has been detected
|
||||
const resultingName = basename
|
||||
? basename.substring(0, basename.length - extname.length)
|
||||
: DEFAULT_BASENAME;
|
||||
let resultingExt = extname;
|
||||
if (!supportedAppExtensions.includes(resultingExt)) {
|
||||
logger.info(
|
||||
`The current file extension '${resultingExt}' is not supported. ` +
|
||||
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
||||
);
|
||||
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
|
||||
}
|
||||
fileName = `${resultingName}${resultingExt}`;
|
||||
}
|
||||
const targetPath = await tempDir.path({
|
||||
prefix: fileName,
|
||||
suffix: '',
|
||||
});
|
||||
newApp = await fetchApp(stream, targetPath);
|
||||
} finally {
|
||||
if (!stream.closed) {
|
||||
stream.destroy();
|
||||
}
|
||||
}
|
||||
if (!fileName) {
|
||||
// assign the default file name and the extension if none has been detected
|
||||
const resultingName = basename
|
||||
? basename.substring(0, basename.length - extname.length)
|
||||
: DEFAULT_BASENAME;
|
||||
let resultingExt = extname;
|
||||
if (!supportedAppExtensions.includes(resultingExt)) {
|
||||
logger.info(
|
||||
`The current file extension '${resultingExt}' is not supported. ` +
|
||||
`Defaulting to '${_.first(supportedAppExtensions)}'`
|
||||
);
|
||||
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
|
||||
}
|
||||
fileName = `${resultingName}${resultingExt}`;
|
||||
}
|
||||
const targetPath = await tempDir.path({
|
||||
prefix: fileName,
|
||||
suffix: '',
|
||||
});
|
||||
newApp = await downloadApp(newApp, targetPath);
|
||||
} else if (await fs.exists(newApp)) {
|
||||
// Use the local app
|
||||
logger.info(`Using local app '${newApp}'`);
|
||||
shouldUnzipApp = ZIP_EXTS.includes(path.extname(newApp));
|
||||
shouldUnzipApp = ZIP_EXTS.has(path.extname(newApp));
|
||||
} else {
|
||||
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
||||
// protocol value for 'C:\\temp' is 'c:', so we check the length as well
|
||||
@@ -411,12 +334,14 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
||||
};
|
||||
|
||||
if (_.isFunction(onPostProcess)) {
|
||||
const result = await onPostProcess({
|
||||
cachedAppInfo: _.clone(cachedAppInfo),
|
||||
isUrl,
|
||||
headers: _.clone(headers),
|
||||
appPath: newApp,
|
||||
});
|
||||
const result = await onPostProcess(
|
||||
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
|
||||
cachedAppInfo: _.clone(cachedAppInfo),
|
||||
isUrl,
|
||||
headers: _.clone(headers),
|
||||
appPath: newApp,
|
||||
})
|
||||
);
|
||||
return !result?.appPath || app === result?.appPath || !(await fs.exists(result?.appPath))
|
||||
? newApp
|
||||
: await storeAppInCache(result.appPath);
|
||||
@@ -429,16 +354,78 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadApp(app, targetPath) {
|
||||
const {href} = url.parse(app);
|
||||
/**
|
||||
* Sends a HTTP GET query to fetch the app with caching enabled.
|
||||
* Follows https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
|
||||
*
|
||||
* @param {string} appLink The URL to download an app from
|
||||
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
|
||||
* @returns {Promise<RemoteAppData>}
|
||||
*/
|
||||
async function queryAppLink(appLink, reqHeaders) {
|
||||
const {href} = url.parse(appLink);
|
||||
/**
|
||||
* @type {import('axios').RawAxiosRequestConfig}
|
||||
*/
|
||||
const requestOpts = {
|
||||
url: href,
|
||||
responseType: 'stream',
|
||||
timeout: APP_DOWNLOAD_TIMEOUT_MS,
|
||||
validateStatus: (status) =>
|
||||
(status >= 200 && status < 300) || status === HTTP_STATUS_NOT_MODIFIED,
|
||||
headers: reqHeaders,
|
||||
};
|
||||
try {
|
||||
await net.downloadFile(href, targetPath, {
|
||||
timeout: APP_DOWNLOAD_TIMEOUT_MS,
|
||||
const {data: stream, headers, status} = await axios(requestOpts);
|
||||
return {
|
||||
stream,
|
||||
headers,
|
||||
status,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Cannot download the app from ${href}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves app payload from the given stream. Also meters the download performance.
|
||||
*
|
||||
* @param {import('stream').Readable} srcStream The incoming stream
|
||||
* @param {string} dstPath The target file path to be written
|
||||
* @returns {Promise<string>} The same dstPath
|
||||
* @throws {Error} If there was a failure while downloading the file
|
||||
*/
|
||||
async function fetchApp(srcStream, dstPath) {
|
||||
const timer = new timing.Timer().start();
|
||||
try {
|
||||
const writer = fs.createWriteStream(dstPath);
|
||||
srcStream.pipe(writer);
|
||||
|
||||
await new B((resolve, reject) => {
|
||||
srcStream.once('error', reject);
|
||||
writer.once('finish', resolve);
|
||||
writer.once('error', (e) => {
|
||||
srcStream.unpipe(writer);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Unable to download the app: ${err.message}`);
|
||||
throw new Error(`Cannot fetch the application: ${err.message}`);
|
||||
}
|
||||
return targetPath;
|
||||
|
||||
const secondsElapsed = timer.getDuration().asSeconds;
|
||||
const {size} = await fs.stat(dstPath);
|
||||
logger.debug(
|
||||
`The application (${util.toReadableSizeString(size)}) ` +
|
||||
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`
|
||||
);
|
||||
// it does not make much sense to approximate the speed for short downloads
|
||||
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
|
||||
const bytesPerSec = Math.floor(size / secondsElapsed);
|
||||
logger.debug(`Approximate download speed: ${util.toReadableSizeString(bytesPerSec)}/s`);
|
||||
}
|
||||
|
||||
return dstPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -606,11 +593,26 @@ export default {
|
||||
parseCapsArray,
|
||||
generateDriverLogPrefix,
|
||||
};
|
||||
export {configureApp, isPackageOrBundle, duplicateKeys, parseCapsArray, generateDriverLogPrefix};
|
||||
export {
|
||||
configureApp,
|
||||
isPackageOrBundle,
|
||||
duplicateKeys,
|
||||
parseCapsArray,
|
||||
generateDriverLogPrefix,
|
||||
BASEDRIVER_VER,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef RemoteAppProps
|
||||
* @property {Date?} lastModified
|
||||
* @property {boolean} immutable
|
||||
* @property {number?} maxAge
|
||||
* @property {string?} etag
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
||||
* @property {number} status The HTTP status of the response
|
||||
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
|
||||
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
|
||||
*/
|
||||
|
||||
@@ -151,8 +151,10 @@ function configureHttp({httpServer, reject, keepAliveTimeout}) {
|
||||
notifier: new EventEmitter(),
|
||||
closed: false,
|
||||
};
|
||||
// TS does not love monkeypatching.
|
||||
const appiumServer = /** @type {AppiumServer} */ (/** @type {unknown} */ (httpServer));
|
||||
/**
|
||||
* @type {AppiumServer}
|
||||
*/
|
||||
const appiumServer = /** @type {any} */ (httpServer);
|
||||
appiumServer.addWebSocketHandler = addWebSocketHandler;
|
||||
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
|
||||
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
|
||||
|
||||
@@ -6,33 +6,21 @@ import B from 'bluebird';
|
||||
const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
|
||||
|
||||
/**
|
||||
* Adds websocket handler to express server instance.
|
||||
* It is expected this function is called in Express
|
||||
* server instance context.
|
||||
*
|
||||
* @this {AppiumServer} - An instance of express HTTP server.
|
||||
* @param {string} handlerPathname - Web socket endpoint path starting with
|
||||
* a single slash character. It is recommended to always add
|
||||
* DEFAULT_WS_PATHNAME_PREFIX to all web socket pathnames.
|
||||
* @param {import('ws').Server} handlerServer - WebSocket server instance. See
|
||||
* https://github.com/websockets/ws/pull/885 for more details
|
||||
* on how to configure the handler properly.
|
||||
* @returns {Promise<void>}
|
||||
* @this {AppiumServer}
|
||||
* @type {AppiumServer['addWebSocketHandler']}
|
||||
*/
|
||||
async function addWebSocketHandler(handlerPathname, handlerServer) {
|
||||
const server = /** @type {AppiumServer} */ (this);
|
||||
if (_.isUndefined(this.webSocketsMapping)) {
|
||||
server.webSocketsMapping = {};
|
||||
this.webSocketsMapping = {};
|
||||
// https://github.com/websockets/ws/pull/885
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
this.on('upgrade', (request, socket, head) => {
|
||||
let currentPathname;
|
||||
try {
|
||||
// @ts-expect-error
|
||||
currentPathname = new URL(request.url).pathname;
|
||||
currentPathname = new URL(/** @type {string} */ (request.url)).pathname;
|
||||
} catch {
|
||||
currentPathname = request.url;
|
||||
}
|
||||
for (const [pathname, wsServer] of _.toPairs(server.webSocketsMapping)) {
|
||||
for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
|
||||
if (currentPathname === pathname) {
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
wsServer.emit('connection', ws, request);
|
||||
@@ -47,17 +35,9 @@ async function addWebSocketHandler(handlerPathname, handlerServer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns web socket handlers registered for the given server
|
||||
* instance.
|
||||
* It is expected this function is called in Express
|
||||
* server instance context.
|
||||
*
|
||||
* @this {AppiumServer}
|
||||
* @param {string?} [keysFilter] - Only include pathnames with given
|
||||
* `keysFilter` value if set. All pairs will be included by default.
|
||||
* @returns {Promise<Record<string, import('ws').Server>>} pathnames to websocket server instances mapping matching the search criteria or an empty object otherwise.
|
||||
* @type {AppiumServer['getWebSocketHandlers']}
|
||||
*/
|
||||
// eslint-disable-next-line require-await
|
||||
async function getWebSocketHandlers(keysFilter = null) {
|
||||
if (_.isEmpty(this.webSocketsMapping)) {
|
||||
return {};
|
||||
@@ -72,16 +52,9 @@ async function getWebSocketHandlers(keysFilter = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes existing websocket handler from express server instance.
|
||||
* The call is ignored if the given `handlerPathname` handler
|
||||
* is not present in the handlers list.
|
||||
* It is expected this function is called in Express
|
||||
* server instance context.
|
||||
* @this {AppiumServer}
|
||||
* @param {string} handlerPathname - Websocket endpoint path.
|
||||
* @returns {Promise<boolean>} true if the handlerPathname was found and deleted
|
||||
* @type {AppiumServer['removeWebSocketHandler']}
|
||||
*/
|
||||
// eslint-disable-next-line require-await
|
||||
async function removeWebSocketHandler(handlerPathname) {
|
||||
const wsServer = this.webSocketsMapping?.[handlerPathname];
|
||||
if (!wsServer) {
|
||||
@@ -103,11 +76,9 @@ async function removeWebSocketHandler(handlerPathname) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all existing websocket handler from express server instance.
|
||||
* It is expected this function is called in Express
|
||||
* server instance context.
|
||||
*
|
||||
* @this {AppiumServer}
|
||||
* @returns {Promise<boolean>} true if at least one handler has been deleted
|
||||
* @type {AppiumServer['removeAllWebSocketHandlers']}
|
||||
*/
|
||||
async function removeAllWebSocketHandlers() {
|
||||
if (_.isEmpty(this.webSocketsMapping)) {
|
||||
|
||||
@@ -83,7 +83,6 @@ export {isW3cCaps, fixCaps};
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
|
||||
*/
|
||||
|
||||
@@ -277,7 +277,7 @@ function routeConfiguringFunction(driver) {
|
||||
`${basePath}${path}`,
|
||||
spec,
|
||||
driver,
|
||||
isSessionCommand(spec.command)
|
||||
isSessionCommand(/** @type {import('@appium/types').DriverMethodDef} */ (spec).command)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -454,7 +454,16 @@ function buildHandler(app, method, path, spec, driver, isSessCmd) {
|
||||
} catch (err) {
|
||||
// if anything goes wrong, figure out what our response should be
|
||||
// based on the type of error that we encountered
|
||||
let actualErr = err;
|
||||
let actualErr;
|
||||
if (err instanceof Error || (_.has(err, 'stack') && _.has(err, 'message'))) {
|
||||
actualErr = err;
|
||||
} else {
|
||||
getLogger(driver, req.params.sessionId || newSessionId).warn(
|
||||
'The thrown error object does not seem to be a valid instance of the Error class. This ' +
|
||||
'might be a genuine bug of a driver or a plugin.'
|
||||
);
|
||||
actualErr = new Error(`${err ?? 'unknown'}`);
|
||||
}
|
||||
|
||||
currentProtocol =
|
||||
currentProtocol || extractProtocol(driver, req.params.sessionId || newSessionId);
|
||||
|
||||
@@ -18,6 +18,7 @@ const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
|
||||
* define the routes, mapping of HTTP methods to particular driver commands, and
|
||||
* any parameters that are expected in a request parameters can be `required` or
|
||||
* `optional`
|
||||
* @satisfies {import('@appium/types').MethodMap<import('../basedriver/driver').BaseDriver>}
|
||||
*/
|
||||
const METHOD_MAP = /** @type {const} */ ({
|
||||
'/status': {
|
||||
@@ -129,7 +130,11 @@ const METHOD_MAP = /** @type {const} */ ({
|
||||
POST: {command: 'deactivateIMEEngine', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/activate': {
|
||||
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}, deprecated: true},
|
||||
POST: {
|
||||
command: 'activateIMEEngine',
|
||||
payloadParams: {required: ['engine']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/frame': {
|
||||
POST: {command: 'setFrame', payloadParams: {required: ['id']}},
|
||||
@@ -356,7 +361,11 @@ const METHOD_MAP = /** @type {const} */ ({
|
||||
DELETE: {command: 'releaseActions'},
|
||||
},
|
||||
'/session/:sessionId/touch/longclick': {
|
||||
POST: {command: 'touchLongClick', payloadParams: {required: ['elements']}, deprecated: true},
|
||||
POST: {
|
||||
command: 'touchLongClick',
|
||||
payloadParams: {required: ['elements']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/touch/flick': {
|
||||
POST: {
|
||||
@@ -922,7 +931,7 @@ const METHOD_MAP = /** @type {const} */ ({
|
||||
let ALL_COMMANDS = [];
|
||||
for (let v of _.values(METHOD_MAP)) {
|
||||
for (let m of _.values(v)) {
|
||||
if (m.command) {
|
||||
if ('command' in m && m.command) {
|
||||
ALL_COMMANDS.push(m.command);
|
||||
}
|
||||
}
|
||||
@@ -1006,13 +1015,3 @@ function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PATH) {
|
||||
const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions'];
|
||||
|
||||
export {METHOD_MAP, ALL_COMMANDS, NO_SESSION_ID_COMMANDS, routeToCommandName};
|
||||
|
||||
/**
|
||||
* This checks compat with the `MethodMap` interface.
|
||||
* @ignore
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _validMethodMap =
|
||||
/** @type {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver<import('@appium/types').BaseDriverCapConstraints>>} */ (
|
||||
METHOD_MAP
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "9.3.3",
|
||||
"version": "9.3.6",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,11 +40,12 @@
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha -p --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:smoke": "node ./index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\"",
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.7",
|
||||
"@appium/types": "^0.10.1",
|
||||
"@appium/support": "^3.1.10",
|
||||
"@appium/types": "^0.10.4",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/async-lock": "1.4.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
@@ -53,7 +54,7 @@
|
||||
"@types/serve-favicon": "2.5.3",
|
||||
"async-lock": "1.4.0",
|
||||
"asyncbox": "2.9.4",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.3.5",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.2",
|
||||
"es6-error": "4.1.1",
|
||||
@@ -65,7 +66,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "3.6.1",
|
||||
"type-fest": "3.8.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -75,8 +76,11 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
|
||||
"typedoc": {
|
||||
"entryPoint": "./lib/index.js"
|
||||
},
|
||||
"tsd": {
|
||||
"directory": "test/types"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {expectAssignable} from 'tsd';
|
||||
// NOTE: this pulls in the distfiles
|
||||
import {BaseDriver} from '.../../..';
|
||||
import {BaseDriverCapConstraints, ExternalDriver, Driver, DriverOpts} from '@appium/types';
|
||||
|
||||
expectAssignable<Driver<BaseDriverCapConstraints>>(
|
||||
new BaseDriver({} as DriverOpts<BaseDriverCapConstraints>)
|
||||
);
|
||||
expectAssignable<ExternalDriver<BaseDriverCapConstraints>>(
|
||||
new BaseDriver({} as DriverOpts<BaseDriverCapConstraints>)
|
||||
);
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.2.6](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.5...@appium/base-plugin@2.2.6) (2023-04-10)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.5](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.4...@appium/base-plugin@2.2.5) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.4](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.3...@appium/base-plugin@2.2.4) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver,base-plugin,types:** update PluginCommand and DriverCommand types ([0dcd5fa](https://github.com/appium/appium/commit/0dcd5fa371af523c6527e55de4cff6cd472fde22))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.3](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.2...@appium/base-plugin@2.2.3) (2023-03-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
@@ -60,7 +60,7 @@ class BasePlugin {
|
||||
* @param {NextPluginCallback} next
|
||||
* @param {Driver<C>} driver
|
||||
* @param {string} script
|
||||
* @param {[Record<string, any>]|[]} protoArgs
|
||||
* @param {readonly [import('@appium/types').StringRecord<unknown>] | readonly any[]} protoArgs
|
||||
*/
|
||||
async executeMethod(next, driver, script, protoArgs) {
|
||||
const Plugin = /** @type {import('@appium/types').PluginClass<Plugin>} */ (this.constructor);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.6",
|
||||
"description": "The base plugin used to create Appium 2.0 plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,8 +40,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.3.3",
|
||||
"@appium/support": "^3.1.7"
|
||||
"@appium/base-driver": "^9.3.6",
|
||||
"@appium/support": "^3.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
@@ -50,7 +50,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
|
||||
"tags": [
|
||||
"appium"
|
||||
],
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.12](https://github.com/appium/appium/compare/@appium/doctor@2.0.11...@appium/doctor@2.0.12) (2023-04-10)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.11](https://github.com/appium/appium/compare/@appium/doctor@2.0.10...@appium/doctor@2.0.11) (2023-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **doctor:** update dependency appium-adb to v9.11.0 ([d8b6334](https://github.com/appium/appium/commit/d8b633406a68e88cdd945906dde3f23a2a566a71))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.10](https://github.com/appium/appium/compare/@appium/doctor@2.0.9...@appium/doctor@2.0.10) (2023-03-28)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.9](https://github.com/appium/appium/compare/@appium/doctor@2.0.8...@appium/doctor@2.0.9) (2023-03-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/doctor",
|
||||
"version": "2.0.9",
|
||||
"version": "2.0.12",
|
||||
"description": "Test environment for fitness to run Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,11 +44,11 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.7",
|
||||
"@appium/support": "^3.1.10",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"appium-adb": "9.10.24",
|
||||
"appium-adb": "9.11.0",
|
||||
"bluebird": "3.7.2",
|
||||
"inquirer": "8.2.5",
|
||||
"lodash": "4.17.21",
|
||||
@@ -63,5 +63,5 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.6](https://github.com/appium/appium/compare/@appium/docutils@0.3.5...@appium/docutils@0.3.6) (2023-04-12)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.5](https://github.com/appium/appium/compare/@appium/docutils@0.3.4...@appium/docutils@0.3.5) (2023-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** properly respects log levels ([e9426fc](https://github.com/appium/appium/commit/e9426fcfecc1d47ab503fc72af4236c4f1ac4b1e))
|
||||
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.4](https://github.com/appium/appium/compare/@appium/docutils@0.3.3...@appium/docutils@0.3.4) (2023-04-05)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.3](https://github.com/appium/appium/compare/@appium/docutils@0.3.2...@appium/docutils@0.3.3) (2023-04-04)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.2](https://github.com/appium/appium/compare/@appium/docutils@0.3.1...@appium/docutils@0.3.2) (2023-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.1](https://github.com/appium/appium/compare/@appium/docutils@0.3.0...@appium/docutils@0.3.1) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
|
||||
* **docutils:** inherit stdio in long-running processes ([ad11fbf](https://github.com/appium/appium/commit/ad11fbf97b57a0c4302d37146660e87c564a5404))
|
||||
* **docutils:** update dependency typedoc to v0.23.27 ([ef9cbe6](https://github.com/appium/appium/commit/ef9cbe6bcc27a3633eda01cd7c08a4466e304630))
|
||||
* **docutils:** update dependency typedoc to v0.23.28 ([dbc0bf6](https://github.com/appium/appium/commit/dbc0bf69e855744ed69ff3b75a9981b313579fa2))
|
||||
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
|
||||
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://github.com/appium/appium/compare/@appium/docutils@0.2.2...@appium/docutils@0.3.0) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import {exec, SubProcess, SubProcessOptions, TeenProcessExecOptions} from 'teen_process';
|
||||
import path from 'node:path';
|
||||
import {exec, TeenProcessExecOptions} from 'teen_process';
|
||||
import {
|
||||
DEFAULT_DEPLOY_BRANCH,
|
||||
DEFAULT_DEPLOY_REMOTE,
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readPackageJson, whichMike} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {argify, stopwatch, TeenProcessSubprocessStartOpts} from '../util';
|
||||
import {getLogger} from '../logger';
|
||||
import {argify, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('builder:deploy');
|
||||
const log = getLogger('builder:deploy');
|
||||
|
||||
/**
|
||||
* Runs `mike serve`
|
||||
@@ -30,15 +30,12 @@ const log = logger.withTag('builder:deploy');
|
||||
*/
|
||||
async function doServe(
|
||||
args: string[] = [],
|
||||
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
|
||||
opts: SpawnBackgroundProcessOpts = {},
|
||||
mikePath?: string
|
||||
) {
|
||||
mikePath = mikePath ?? (await whichMike());
|
||||
const finalArgs = ['serve', ...args];
|
||||
const opts: SubProcessOptions = {stdio: 'inherit'};
|
||||
log.debug('Launching %s with args: %s', mikePath, finalArgs);
|
||||
const proc = new SubProcess(mikePath, finalArgs, opts);
|
||||
return await proc.start(startDetector, detach, timeoutMs);
|
||||
return spawnBackgroundProcess(mikePath, finalArgs, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +124,7 @@ export async function deploy({
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mikeArgs, serveOpts);
|
||||
} else {
|
||||
log.info('Deploying into branch %s', branch);
|
||||
const mikeArgs = [
|
||||
...argify(
|
||||
_.omitBy(
|
||||
@@ -141,7 +139,7 @@ export async function deploy({
|
||||
}
|
||||
await doDeploy(mikeArgs, execOpts);
|
||||
|
||||
log.success('Mike finished deployment into branch %s (%dms)', branch, stop());
|
||||
log.success('Finished deployment into branch %s (%dms)', branch, stop());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +221,7 @@ export interface DeployOpts {
|
||||
execOpts?: TeenProcessExecOptions;
|
||||
|
||||
/**
|
||||
* Extra options for {@linkcode teen_process.Subprocess.start}
|
||||
* Extra options for {@linkcode spawnBackgroundProcess}
|
||||
*/
|
||||
serveOpts?: TeenProcessSubprocessStartOpts;
|
||||
serveOpts?: SpawnBackgroundProcessOpts;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ import {
|
||||
safeWriteFile,
|
||||
stringifyYaml,
|
||||
} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
import {MkDocsYml, MkDocsYmlNav} from '../model';
|
||||
import {relative} from '../util';
|
||||
|
||||
const log = logger.withTag('builder:nav');
|
||||
const log = getLogger('builder:nav');
|
||||
|
||||
/**
|
||||
* Gets a list of `.md` files relative to `docs_dir`
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findTypeDocJsonPath, readTypedocJson} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
import {argify, relative, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('builder:reference');
|
||||
const log = getLogger('builder:reference');
|
||||
|
||||
/**
|
||||
* Executes TypeDoc _in the current process_
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import {exec, SubProcess, SubProcessOptions, TeenProcessExecOptions} from 'teen_process';
|
||||
import {NAME_BIN, NAME_THEME, NAME_MKDOCS_YML} from '../constants';
|
||||
import {exec, TeenProcessExecOptions} from 'teen_process';
|
||||
import {NAME_BIN, NAME_MKDOCS_YML, NAME_THEME} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readMkDocsYml, whichMkDocs} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {relative, stopwatch, TeenProcessSubprocessStartOpts} from '../util';
|
||||
import {getLogger} from '../logger';
|
||||
import {relative, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('mkdocs');
|
||||
const log = getLogger('mkdocs');
|
||||
|
||||
/**
|
||||
* Runs `mkdocs serve`
|
||||
@@ -23,15 +23,14 @@ const log = logger.withTag('mkdocs');
|
||||
*/
|
||||
async function doServe(
|
||||
args: string[] = [],
|
||||
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
|
||||
opts: SpawnBackgroundProcessOpts = {},
|
||||
mkDocsPath?: string
|
||||
) {
|
||||
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
|
||||
const finalArgs = ['serve', ...args];
|
||||
const opts: SubProcessOptions = {stdio: 'inherit'};
|
||||
|
||||
log.debug('Launching %s with args: %s', mkDocsPath, finalArgs);
|
||||
const proc = new SubProcess(mkDocsPath, finalArgs, opts);
|
||||
return await proc.start(startDetector, detach, timeoutMs);
|
||||
return spawnBackgroundProcess(mkDocsPath, finalArgs, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +80,7 @@ export async function buildSite({
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mkdocsArgs, serveOpts);
|
||||
} else {
|
||||
log.info('Building site into %s (%dms)');
|
||||
await doBuild(mkdocsArgs, execOpts);
|
||||
let relSiteDir;
|
||||
if (siteDir) {
|
||||
@@ -90,7 +90,7 @@ export async function buildSite({
|
||||
log.debug('Found site_dir %s', siteDir);
|
||||
relSiteDir = relative(path.dirname(mkDocsYmlPath), siteDir!);
|
||||
}
|
||||
log.success('MkDocs finished building into %s (%dms)', relSiteDir, stop());
|
||||
log.success('Finnished building site into %s (%dms)', relSiteDir, stop());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export interface BuildMkDocsOpts {
|
||||
execOpts?: TeenProcessExecOptions;
|
||||
|
||||
/**
|
||||
* Extra options for {@linkcode teen_process.Subprocess.start}
|
||||
* Extra options for {@linkcode spawnBackgroundProcess}
|
||||
*/
|
||||
serveOpts?: TeenProcessSubprocessStartOpts;
|
||||
serveOpts?: SpawnBackgroundProcessOpts;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import {fs, util} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import type {Options} from 'yargs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
|
||||
const log = logger.withTag('check');
|
||||
const log = getLogger('check');
|
||||
|
||||
/**
|
||||
* Given a list of objects with `id` and `path` props, filters out the ones that do not exist
|
||||
|
||||
@@ -7,11 +7,11 @@ import path from 'node:path';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {buildReferenceDocs, buildSite, deploy, updateNav} from '../../builder';
|
||||
import {NAME_BIN} from '../../constants';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {stopwatch} from '../../util';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('build');
|
||||
const log = getLogger('build');
|
||||
|
||||
enum BuildCommandGroup {
|
||||
Build = 'Build Config:',
|
||||
@@ -220,6 +220,7 @@ export default {
|
||||
);
|
||||
},
|
||||
async handler(args) {
|
||||
log.info('Building docs...');
|
||||
const stop = stopwatch('build');
|
||||
log.debug('Build command called with args: %O', args);
|
||||
if (!args.site && !args.reference) {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
import _ from 'lodash';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {init} from '../../init';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {stopwatch} from '../../util';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const log = getLogger('init');
|
||||
|
||||
enum InitCommandGroup {
|
||||
MkDocs = 'MkDocs Config:',
|
||||
|
||||
@@ -7,10 +7,10 @@ import {util} from '@appium/support';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {DocutilsError} from '../../error';
|
||||
import {DocutilsValidator, ValidationKind} from '../../validate';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('validate');
|
||||
const log = getLogger('validate');
|
||||
|
||||
enum ValidateCommandGroup {
|
||||
Behavior = 'Validation Behavior:',
|
||||
|
||||
@@ -11,10 +11,11 @@ import path from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
import parser from 'yargs-parser';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import {NAME_BIN} from '../constants';
|
||||
import logger from '../logger';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap, NAME_BIN} from '../constants';
|
||||
import {getLogger, initLogger, isLogLevelString} from '../logger';
|
||||
import {relative} from '../util';
|
||||
const log = logger.withTag('config');
|
||||
|
||||
const log = getLogger('config');
|
||||
|
||||
/**
|
||||
* `lilconfig` loader for YAML
|
||||
@@ -43,8 +44,19 @@ const loadEsm: Loader = (filepath: string) => import(filepath);
|
||||
export async function findConfig(argv: string[] = hideBin(process.argv)) {
|
||||
const preArgs = parser(argv);
|
||||
|
||||
if (preArgs.verbose || preArgs.logLevel === 'debug') {
|
||||
log.level = LogLevel.Debug;
|
||||
// if --verbose is used, set the log level to debug.
|
||||
// otherwise use --log-level or the default.
|
||||
let logLevel: LogLevel | keyof typeof LogLevelMap;
|
||||
if (preArgs.verbose) {
|
||||
logLevel = LogLevel.Debug;
|
||||
} else {
|
||||
// if the loglevel is valid, use it, otherwise use the default
|
||||
logLevel = isLogLevelString(preArgs.logLevel) ? preArgs.logLevel : DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
initLogger(logLevel);
|
||||
|
||||
if (preArgs.noConfig) {
|
||||
log.debug('Not loading config because --no-config was provided');
|
||||
}
|
||||
|
||||
return preArgs.noConfig || preArgs.help || preArgs.version
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
@@ -20,7 +20,7 @@ import {sync as readPkg} from 'read-pkg';
|
||||
|
||||
const pkg = readPkg({cwd: fs.findRoot(__dirname)});
|
||||
|
||||
const log = logger.withTag('cli');
|
||||
const log = getLogger('cli');
|
||||
export async function main(argv = hideBin(process.argv)) {
|
||||
const config = await findConfig(argv);
|
||||
|
||||
@@ -56,24 +56,14 @@ export async function main(argv = hideBin(process.argv)) {
|
||||
describe: 'Disable config file discovery',
|
||||
},
|
||||
})
|
||||
.middleware([
|
||||
.middleware(
|
||||
/**
|
||||
* Configures logging; `--verbose` implies `--log-level=debug`
|
||||
* Writes a startup message
|
||||
*/
|
||||
(argv) => {
|
||||
if (argv.verbose) {
|
||||
argv.logLevel = 'debug';
|
||||
log.debug('Debug logging enabled via --verbose');
|
||||
}
|
||||
log.level = LogLevelMap[argv.logLevel];
|
||||
},
|
||||
/**
|
||||
* Writes a startup message, if logging is enabled
|
||||
*/
|
||||
async () => {
|
||||
() => {
|
||||
log.info(`${pkg.name} @ v${pkg.version} (Node.js ${process.version})`);
|
||||
},
|
||||
])
|
||||
}
|
||||
)
|
||||
.epilog(`Please report bugs at ${pkg.bugs?.url}`)
|
||||
.fail(
|
||||
/**
|
||||
@@ -85,8 +75,7 @@ export async function main(argv = hideBin(process.argv)) {
|
||||
log.error(error.message);
|
||||
} else {
|
||||
y.showHelp();
|
||||
console.log();
|
||||
log.error(msg ?? error.message);
|
||||
log.error(`\n\n${msg ?? error.message}`);
|
||||
}
|
||||
y.exit(1, error);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {fs} from '@appium/support';
|
||||
import * as JSON5 from 'json5';
|
||||
import _ from 'lodash';
|
||||
import _pkgDir from 'pkg-dir';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {Application, TypeDocReader} from 'typedoc';
|
||||
import {
|
||||
NAME_TYPEDOC_JSON,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import {DocutilsError} from './error';
|
||||
import {MkDocsYml} from './model';
|
||||
|
||||
const log = logger.withTag('fs');
|
||||
const log = getLogger('fs');
|
||||
|
||||
/**
|
||||
* Finds path to closest `package.json`
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './builder';
|
||||
export * from './validate';
|
||||
export * from './scaffold';
|
||||
export * from './constants';
|
||||
export * from './logger';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {exec} from 'teen_process';
|
||||
import {Simplify} from 'type-fest';
|
||||
import {DocutilsError} from './error';
|
||||
import {createScaffoldTask, ScaffoldTaskOptions} from './scaffold';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, TsConfigJson, TypeDocJson} from './model';
|
||||
import _ from 'lodash';
|
||||
import {stringifyJson5, stringifyYaml} from './fs';
|
||||
@@ -54,8 +54,8 @@ const BASE_TSCONFIG_JSON: Readonly<TsConfigJson> = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const dryRunLog = log.withTag('dry-run');
|
||||
const log = getLogger('init');
|
||||
const dryRunLog = getLogger('dry-run', log);
|
||||
|
||||
const DEFAULT_INCLUDE = ['lib', 'test', 'index.js'];
|
||||
/**
|
||||
|
||||
@@ -134,60 +134,61 @@ class DocutilsReporter extends FancyReporter {
|
||||
*
|
||||
* "Global" inasmuch as any logger created from the root logger will use this level.
|
||||
*/
|
||||
let globalLevel = LogLevelMap[DEFAULT_LOG_LEVEL];
|
||||
let globalLevel: LogLevel = LogLevelMap[DEFAULT_LOG_LEVEL];
|
||||
|
||||
/**
|
||||
* Type guard to see if a string is a recognized log level
|
||||
* @param level any value
|
||||
*/
|
||||
export function isLogLevelString(level: any): level is keyof typeof LogLevelMap {
|
||||
return level in LogLevelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The logger from which all loggers are created. This one uses a unique tag and our custom reporter.
|
||||
*/
|
||||
const rootLogger = createLogProxy(
|
||||
consola.create({defaults: {tag: 'docutils'}, reporters: [new DocutilsReporter()]})
|
||||
);
|
||||
const rootLogger = consola.create({
|
||||
defaults: {tag: 'docutils'},
|
||||
reporters: [new DocutilsReporter()],
|
||||
level: globalLevel,
|
||||
});
|
||||
// this prevents logging before `initLogger` is called
|
||||
rootLogger.pause();
|
||||
|
||||
/**
|
||||
* @summary Creates a log-level-propagating proxy for a {@linkcode Consola} logger.
|
||||
* @description
|
||||
* Alright. So when you create a new logger via {@linkcode Consola.create}, it's basically a clone
|
||||
* of its parent with a new set of options.
|
||||
*
|
||||
* If we change the log level of the root logger (which we do: see `cli/index.ts`), we may (almost
|
||||
* certainly) have
|
||||
* child loggers which: a) have already been created and b) have inherited the old/default log level
|
||||
* from the root logger. We don't _want_ that (though this is likely a reasonable use case) for our
|
||||
* purposes.
|
||||
*
|
||||
* The implementation below solves the problem by maintaining its own singleton log level value, and
|
||||
* intercepts the `level` property of any logger created from the root logger.
|
||||
*
|
||||
* There are other ways to go about this which may be better, but this seemed pretty straightforward.
|
||||
* A map of tags to loggers
|
||||
*/
|
||||
function createLogProxy(logger: Consola): Consola {
|
||||
return new Proxy(logger, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'level') {
|
||||
return globalLevel;
|
||||
}
|
||||
if (prop === 'create') {
|
||||
const create = Reflect.get(target, prop, receiver) as Consola['create'];
|
||||
return (opts: ConsolaOptions) => createLogProxy(create.call(receiver, opts));
|
||||
}
|
||||
if (prop === '_defaults') {
|
||||
const defaults = Reflect.get(target, prop, receiver);
|
||||
return {...defaults, level: globalLevel};
|
||||
}
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
set(target, prop, value, receiver) {
|
||||
if (prop === 'level') {
|
||||
globalLevel = value as LogLevel;
|
||||
return true;
|
||||
}
|
||||
return Reflect.set(target, prop, value, receiver);
|
||||
},
|
||||
});
|
||||
const loggers: Map<string, WeakRef<Consola>> = new Map();
|
||||
|
||||
export function getLogger(tag: string, parent = rootLogger) {
|
||||
if (loggers.has(tag)) {
|
||||
const logger = loggers.get(tag)?.deref();
|
||||
if (logger) {
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
const logger = parent.withTag(tag);
|
||||
logger.level = globalLevel;
|
||||
loggers.set(tag, new WeakRef(logger));
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The proxied root logger
|
||||
* @see {createLogProxy}
|
||||
* Initialize the logging system.
|
||||
*
|
||||
* This should only be called once. The loglevel cannot be changed once it is set.
|
||||
*
|
||||
* @remarks Child loggers seem to inherit the "paused" state of the parent, so when this is called, we must resume all of them.
|
||||
*/
|
||||
export default rootLogger;
|
||||
export const initLogger = _.once((level: keyof typeof LogLevelMap | LogLevel) => {
|
||||
globalLevel = isLogLevelString(level) ? LogLevelMap[level] : level;
|
||||
rootLogger.level = globalLevel;
|
||||
rootLogger.resume();
|
||||
for (const ref of loggers.values()) {
|
||||
const logger = ref.deref();
|
||||
if (logger) {
|
||||
logger.level = globalLevel;
|
||||
logger.resume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import {fs} from '@appium/support';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import path from 'node:path';
|
||||
import {createPatch} from 'diff';
|
||||
import {NormalizedPackageJson} from 'read-pkg';
|
||||
@@ -15,8 +15,8 @@ import _ from 'lodash';
|
||||
import {stringifyJson, readPackageJson, safeWriteFile} from './fs';
|
||||
import {NAME_ERR_ENOENT, NAME_ERR_EEXIST} from './constants';
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const dryRunLog = log.withTag('dry-run');
|
||||
const log = getLogger('init');
|
||||
const dryRunLog = getLogger('dry-run', log);
|
||||
|
||||
/**
|
||||
* Creates a unified patch for display in "dry run" mode
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import {SpawnOptions, spawn} from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import type {SubProcess} from 'teen_process';
|
||||
|
||||
@@ -72,8 +73,27 @@ export const argify: (obj: Record<string, string | number | boolean | undefined>
|
||||
);
|
||||
|
||||
/**
|
||||
* Conversion of the parameters of {@linkcode SubProcess.start} to an object.
|
||||
* Spawns a long-running "background" child process. This is expected to only return control to the
|
||||
* parent process in the case of a nonzero exit code from the child process.
|
||||
* @param command Command to run
|
||||
* @param args Args to pass to command
|
||||
* @param opts Spawn options. `{stdio: 'inherit'}` will always be true
|
||||
* @privateRemarks `teen_process` is good for running a one-shot command, but not so great for
|
||||
* background tasks; we use node's `child_process` directly here to pass `stdio` through, since
|
||||
* `teen_process` basically does not respect `{stdio: 'inherit'}`.
|
||||
*/
|
||||
export type TeenProcessSubprocessStartOpts = Partial<
|
||||
TupleToObject<Parameters<SubProcess['start']>, ['startDetector', 'detach', 'timeoutMs']>
|
||||
>;
|
||||
export async function spawnBackgroundProcess(command: string, args: string[], opts: SpawnOptions) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
spawn(command, args, {...opts, stdio: 'inherit'})
|
||||
.on('error', reject)
|
||||
.on('close', (code) => {
|
||||
// can be null or number
|
||||
if (code) {
|
||||
return reject(new Error(`${command} exited with code ${code}`));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export type SpawnBackgroundProcessOpts = Omit<SpawnOptions, 'stdio'>;
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
whichPython,
|
||||
readMkDocsYml,
|
||||
} from './fs';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, PipPackage, TypeDocJson} from './model';
|
||||
import {relative} from './util';
|
||||
|
||||
@@ -64,7 +64,7 @@ const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
|
||||
*/
|
||||
const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
|
||||
|
||||
const log = logger.withTag('validate');
|
||||
const log = getLogger('validate');
|
||||
|
||||
/**
|
||||
* The "kinds" of validation which were requested to be performed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.6",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -49,9 +49,9 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.7",
|
||||
"@appium/tsconfig": "^0.2.4",
|
||||
"@appium/typedoc-plugin-appium": "^0.5.1",
|
||||
"@appium/support": "^3.1.10",
|
||||
"@appium/tsconfig": "^0.3.0",
|
||||
"@appium/typedoc-plugin-appium": "^0.6.2",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "2.15.3",
|
||||
@@ -67,8 +67,8 @@
|
||||
"semver": "7.3.8",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.6.1",
|
||||
"typedoc": "0.23.26",
|
||||
"type-fest": "3.8.0",
|
||||
"typedoc": "0.23.28",
|
||||
"typedoc-plugin-markdown": "3.14.0",
|
||||
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
|
||||
"typescript": "4.9.5",
|
||||
@@ -83,5 +83,5 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.1.1
|
||||
mkdocs-material==9.1.6
|
||||
mkdocs-material-extensions==1.1.1
|
||||
mike==1.1.2
|
||||
|
||||
@@ -3,6 +3,45 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.4.4](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.3...@appium/driver-test-support@0.4.4) (2023-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
|
||||
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.3](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.2...@appium/driver-test-support@0.4.3) (2023-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.2](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.1...@appium/driver-test-support@0.4.2) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
|
||||
* **driver-test-support:** update definitelytyped ([86eb495](https://github.com/appium/appium/commit/86eb495641456e3e467f507e4d230ad1bc2901c1))
|
||||
* **test-support:** update dependency sinon to v15.0.2 ([f1a8ce0](https://github.com/appium/appium/commit/f1a8ce0bbac5a165ba5175c50d83668ec305b592))
|
||||
* **test-support:** update dependency sinon to v15.0.3 ([d774a84](https://github.com/appium/appium/commit/d774a8428a0f0c4ecbbc8124a855bda12821ae08))
|
||||
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
|
||||
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.1](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.0...@appium/driver-test-support@0.4.1) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -425,7 +425,12 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
|
||||
* @typedef {import('@appium/types').BaseNSCapabilities} BaseNSCapabilities
|
||||
* @typedef {import('axios').RawAxiosRequestConfig} RawAxiosRequestConfig
|
||||
* @typedef {import('@appium/types').SingularSessionData} SingularSessionData
|
||||
*/
|
||||
|
||||
/**
|
||||
* `Constraints` is purposefully loose here
|
||||
* @template {Constraints} [C=Constraints]
|
||||
* @typedef {import('@appium/types').SingularSessionData<C>} SingularSessionData
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -434,18 +439,18 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* `Constraints` is purposefully loose here
|
||||
* @template {Constraints} [C=Constraints]
|
||||
* @typedef NewSessionData
|
||||
* @property {import('type-fest').RequireAtLeastOne<import('@appium/types').W3CCapabilities<C, Extra>, 'firstMatch'|'alwaysMatch'>} capabilities
|
||||
* @property {import('type-fest').RequireAtLeastOne<import('@appium/types').W3CCapabilities<C>, 'firstMatch'|'alwaysMatch'>} capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=BaseDriverCapConstraints]
|
||||
* @template {StringRecord|void} [Extra=void]
|
||||
* `Constraints` is purposefully loose here
|
||||
* @template {Constraints} [C=Constraints]
|
||||
* @typedef NewSessionResponse
|
||||
* @property {string} sessionId,
|
||||
* @property {import('@appium/types').Capabilities<C, Extra>} capabilities
|
||||
* @property {import('@appium/types').Capabilities<C>} capabilities
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ export * from './stoppable';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {import('@appium/types').Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
|
||||
* @template {import('@appium/types').StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
|
||||
* @template {import('@appium/types').Constraints} C
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
|
||||
*/
|
||||
|
||||
@@ -12,18 +12,22 @@ const {expect} = chai;
|
||||
|
||||
/**
|
||||
* Creates unit test suites for a driver.
|
||||
* @param {DriverClass} DriverClass
|
||||
* @param {Partial<BaseNSCapabilities>} [defaultCaps]
|
||||
* @template {Constraints} C
|
||||
* @param {DriverClass<C>} DriverClass
|
||||
* @param {import('@appium/types').NSDriverCaps<C>} [defaultCaps]
|
||||
*/
|
||||
|
||||
export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
export function driverUnitTestSuite(
|
||||
DriverClass,
|
||||
defaultCaps = /** @type {import('@appium/types').NSDriverCaps<C>} */ ({})
|
||||
) {
|
||||
// to display the driver under test in report
|
||||
const className = DriverClass.name ?? '(unknown driver)';
|
||||
|
||||
describe(`BaseDriver unit suite (as ${className})`, function () {
|
||||
/** @type {InstanceType<typeof DriverClass>} */
|
||||
let d;
|
||||
/** @type {W3CCapabilities} */
|
||||
/** @type {import('@appium/types').W3CDriverCaps<C>} */
|
||||
let w3cCaps;
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
let sandbox;
|
||||
@@ -229,7 +233,7 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
});
|
||||
|
||||
describe('command queue', function () {
|
||||
/** @type {InstanceType<DriverClass>} */
|
||||
/** @type {InstanceType<DriverClass<Constraints>>} */
|
||||
let d;
|
||||
let waitMs = 10;
|
||||
|
||||
@@ -531,11 +535,11 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
d.logEvent('commands');
|
||||
}).should.throw();
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error - bad type
|
||||
d.logEvent(1);
|
||||
}).should.throw();
|
||||
(() => {
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error - bad type
|
||||
d.logEvent({});
|
||||
}).should.throw();
|
||||
});
|
||||
@@ -631,7 +635,7 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').DriverClass<Driver<C>>} DriverClass
|
||||
*/
|
||||
|
||||
@@ -641,7 +645,6 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
|
||||
* @template {import('@appium/types').StringRecord|void} [Extra=void]
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
|
||||
* @template {Constraints} C
|
||||
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.4",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,18 +41,18 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.10.1",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@appium/types": "^0.10.4",
|
||||
"@types/lodash": "4.14.192",
|
||||
"@types/stoppable": "1.1.1",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.3.5",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "4.3.7",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "15.0.1",
|
||||
"sinon": "15.0.3",
|
||||
"source-map-support": "0.5.21",
|
||||
"stoppable": "1.1.0",
|
||||
"type-fest": "3.6.1"
|
||||
"type-fest": "3.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
@@ -65,5 +65,8 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
|
||||
"typedoc": {
|
||||
"entryPoint": "./lib/index.js"
|
||||
},
|
||||
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@0.3.0...@appium/eslint-config-appium-ts@0.3.1) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/eslint-config-appium-ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@0.2.0...@appium/eslint-config-appium-ts@0.3.0) (2023-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **eslint-config-appium-ts:** disable brace-style rule ([409192d](https://github.com/appium/appium/commit/409192d0630244e072534391eb702de063d54d17))
|
||||
|
||||
|
||||
* feat(eslint-config-appium-ts)!: enable for JS sources ([43df77f](https://github.com/appium/appium/commit/43df77f6b623462d20d90cb23a2d7577aa0c56de))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This makes `@appium/eslint-config-appium-ts` also apply to JS sources _and_ requires a new peer dependency (`eslint-import-resolver-typescript`). The `main` file is now a `.js` file. The module is expected to _extend_ `@appium/eslint-config-appium`, not replace it; consumers needn't extend both in their ESLint config.
|
||||
|
||||
In the not-too-distant past, `@typescript-eslint/parser` was unable to understand docstrings in `.js` files. Evidently this is no longer true.
|
||||
|
||||
I've rewritten and heavily commented the configuration explaining the intent behind everything.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.2.0 (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* `@appium/eslint-config-appium-ts` is a configuration for ESLint which extends
|
||||
* `@appium/eslint-config-appium` and adds TypeScript support.
|
||||
*
|
||||
* It is **not** a _replacement for_ `@appium/eslint-config-appium`.
|
||||
*
|
||||
* It can be used _without any `.ts` sources_, as long as a `tsconfig.json` exists in the project
|
||||
* root. In that case, it will run on `.js` files which are enabled for checking; this includes the
|
||||
* `checkJs` setting and any `// @ts-check` directive in source files.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
$schema: 'http://json.schemastore.org/eslintrc',
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['@appium/eslint-config-appium', 'plugin:@typescript-eslint/recommended'],
|
||||
rules: {
|
||||
/**
|
||||
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
|
||||
* without explanation.
|
||||
* @remarks It's good practice to explain why things break!
|
||||
*/
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'warn',
|
||||
{
|
||||
'ts-expect-error': 'allow-with-description',
|
||||
'ts-ignore': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
/**
|
||||
* Empty functions are allowed.
|
||||
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
|
||||
*/
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
/**
|
||||
* Empty interfaces are allowed.
|
||||
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
|
||||
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
|
||||
*/
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
/**
|
||||
* Explicit `any` types are allowed.
|
||||
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
|
||||
*/
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
/**
|
||||
* Warns if a non-null assertion (`!`) is used.
|
||||
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
|
||||
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
|
||||
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
|
||||
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
|
||||
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
|
||||
* non-null assertion.
|
||||
*/
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
/**
|
||||
* This disallows use of `require()`.
|
||||
* @remarks We _do_ use `require()` fairly often to load files on-the-fly; however, these may
|
||||
* want to be replaced with `import()` (I am not sure if there's a rule about that?). **If this check fails**, disable the rule for the particular line.
|
||||
*/
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
// 'no-unused-vars': 'off',
|
||||
/**
|
||||
* Allow native `Promise`s. **This overrides `@appium/eslint-config-appium`.**
|
||||
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
|
||||
* everywhere, but this is not strictly necessary.
|
||||
*/
|
||||
'promise/no-native': 'off',
|
||||
/**
|
||||
* Allow `async` functions without `await`. **This overrides `@appium/eslint-config-appium`.**
|
||||
* @remarks Originally, this was to be more clear about the return value of a function, but with
|
||||
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
|
||||
* `return await somePromise` have their own use-cases.
|
||||
*/
|
||||
'require-await': 'off',
|
||||
|
||||
/**
|
||||
* Disables the `brace-style` rule.
|
||||
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
|
||||
* to indent in a way that is not allowed by the default `brace-style` rule.
|
||||
*/
|
||||
'brace-style': 'off',
|
||||
},
|
||||
/**
|
||||
* This stuff enables `eslint-plugin-import` to resolve TS modules.
|
||||
*/
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['tsconfig.json', './packages/*/tsconfig.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
/**
|
||||
* Overrides for tests.
|
||||
*/
|
||||
{
|
||||
files: ['**/test/**', '*.spec.js', '-specs.js', '*.spec.ts'],
|
||||
rules: {
|
||||
/**
|
||||
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
|
||||
* @remarks We often test things which explicitly violate types.
|
||||
*/
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
/**
|
||||
* Allow non-null assertions in tests; do not even warn.
|
||||
* @remarks The idea is that the assertions themselves will be written in such a way that if
|
||||
* the non-null assertion was invalid, the assertion would fail.
|
||||
*/
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user