mirror of
https://github.com/appium/appium.git
synced 2026-11-02 15:18:24 +08:00
Compare commits
184
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af066eb5c3 | ||
|
|
e5254b5b45 | ||
|
|
c18eb03261 | ||
|
|
d0ebbaa0c6 | ||
|
|
1629f858f8 | ||
|
|
2d124323c5 | ||
|
|
901ac711c4 | ||
|
|
7982747677 | ||
|
|
26bb0f95b9 | ||
|
|
c161ec2bad | ||
|
|
1c4737c8b4 | ||
|
|
6b2f5668e9 | ||
|
|
d904477b4c | ||
|
|
da1da3fe13 | ||
|
|
307c81a24b | ||
|
|
f4c37985eb | ||
|
|
78c6794253 | ||
|
|
c416a86120 | ||
|
|
38f86900da | ||
|
|
40fc097eda | ||
|
|
0097c8e52e | ||
|
|
1ccf857a4b | ||
|
|
c34f2eb897 | ||
|
|
1d7c58ff26 | ||
|
|
d78ca63e60 | ||
|
|
4f171ee92f | ||
|
|
fd2c72fd88 | ||
|
|
3bf406015e | ||
|
|
56499eb997 | ||
|
|
bd10bffec4 | ||
|
|
b1ee41cfc7 | ||
|
|
cc7f5f4a52 | ||
|
|
a98d9c33c7 | ||
|
|
1a2260bb15 | ||
|
|
4c52fdf1d0 | ||
|
|
a959eec9cf | ||
|
|
e96724077e | ||
|
|
dfae94e7f3 | ||
|
|
0497090a90 | ||
|
|
19277f6e14 | ||
|
|
39a59c8bb3 | ||
|
|
2766dcca4c | ||
|
|
f5b112bcca | ||
|
|
5d6458b611 | ||
|
|
d47c805468 | ||
|
|
4c61d533b8 | ||
|
|
325d237207 | ||
|
|
8f63dabb5d | ||
|
|
5a555090c5 | ||
|
|
81ed72f19d | ||
|
|
f34c75c650 | ||
|
|
39d68f17a2 | ||
|
|
528dcfe324 | ||
|
|
4d7f39b5f8 | ||
|
|
8594d13bf6 | ||
|
|
a0b434dff7 | ||
|
|
19a5ae4537 | ||
|
|
fc831d2475 | ||
|
|
cb1a97fc8e | ||
|
|
a506a0a357 | ||
|
|
63a429c53b | ||
|
|
922725b4f1 | ||
|
|
f0ebe2e02f | ||
|
|
2ca1581870 | ||
|
|
6aec50b074 | ||
|
|
426830d14a | ||
|
|
9538c34131 | ||
|
|
5d24ea1ad2 | ||
|
|
db73971018 | ||
|
|
7b17754786 | ||
|
|
ba07d2a854 | ||
|
|
ee365ad168 | ||
|
|
5e351dc503 | ||
|
|
f512e596eb | ||
|
|
3eec0227a2 | ||
|
|
ea42548442 | ||
|
|
e1fa6c2133 | ||
|
|
6b245534c2 | ||
|
|
6e4d992b8b | ||
|
|
ac4d4f7262 | ||
|
|
d891c9570a | ||
|
|
37e6376d10 | ||
|
|
5ceaa5b38a | ||
|
|
cfa7b2d442 | ||
|
|
c8d707df90 | ||
|
|
40e5199caa | ||
|
|
cdfe7c3d82 | ||
|
|
3c4d3acc09 | ||
|
|
2f26c51dac | ||
|
|
155f24bb4d | ||
|
|
74f29cfd5e | ||
|
|
ac85f051a7 | ||
|
|
e68f17b3cc | ||
|
|
25fd473b98 | ||
|
|
55060f8e8f | ||
|
|
1da9de4225 | ||
|
|
6f10078591 | ||
|
|
c311c48c55 | ||
|
|
869b7aa4a2 | ||
|
|
82e2150c66 | ||
|
|
94a207fc97 | ||
|
|
c568523e01 | ||
|
|
91a6bc5925 | ||
|
|
6499eea0af | ||
|
|
add0eaa05f | ||
|
|
3d1e4af858 | ||
|
|
e7df6fd4f0 | ||
|
|
c37072e6a9 | ||
|
|
864ab11b8a | ||
|
|
4f564ef7ad | ||
|
|
b2bda2188b | ||
|
|
ddadd6977d | ||
|
|
fa681f6355 | ||
|
|
522177f1d3 | ||
|
|
ab7958c8e8 | ||
|
|
1e4bc0a273 | ||
|
|
9f8b9cef70 | ||
|
|
2fb844c8ad | ||
|
|
2796639b11 | ||
|
|
65f74d818f | ||
|
|
cf398bdfd6 | ||
|
|
ddfca214bf | ||
|
|
0f19902918 | ||
|
|
9ad7279fa7 | ||
|
|
66922279b7 | ||
|
|
61ca898c47 | ||
|
|
d3c30e5079 | ||
|
|
35b5ac72ab | ||
|
|
a4a816efeb | ||
|
|
93bddb4c73 | ||
|
|
8627d75a47 | ||
|
|
45cd43488c | ||
|
|
c85b89ecbe | ||
|
|
1f9a625f00 | ||
|
|
b9ef676b73 | ||
|
|
66459e991e | ||
|
|
5a70d7ce78 | ||
|
|
611a63f0ae | ||
|
|
e4863778c7 | ||
|
|
0ff735598a | ||
|
|
5d705851f2 | ||
|
|
6ba5c52e98 | ||
|
|
a314166585 | ||
|
|
fe96af91f2 | ||
|
|
52ae548ddc | ||
|
|
9214c15d58 | ||
|
|
11406e4879 | ||
|
|
640e0ae30f | ||
|
|
6245022be9 | ||
|
|
91abd5b598 | ||
|
|
78bc0926db | ||
|
|
59e4367cc8 | ||
|
|
c7d9a456d8 | ||
|
|
e8590c77e7 | ||
|
|
6ae86bd5c7 | ||
|
|
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 |
@@ -10,12 +10,6 @@
|
||||
"files": "packages/*/test/**/*",
|
||||
"rules": {"func-names": "off"}
|
||||
},
|
||||
{
|
||||
"files": ["packages/*/index.js", "packages/*/scripts/**/*.js", "test/*.js"],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/appium/support.js",
|
||||
@@ -27,9 +21,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["./test/setup.js", "./**/scripts/**/*.js", "./packages/*/index.js"],
|
||||
"files": [
|
||||
"./test/setup.js",
|
||||
"./**/scripts/**/*.js",
|
||||
"./packages/*/index.js",
|
||||
"./packages/docutils/bin/appium-docs.js"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -147,3 +147,8 @@ labels:
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/universal-xml-plugin/**']
|
||||
|
||||
- label: '@appium/strongbox'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/strongbox/**']
|
||||
|
||||
+18
-10
@@ -44,19 +44,31 @@ permissions:
|
||||
jobs:
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
# TODO: Enable below envs after all tests have been verified green
|
||||
# - macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- name: Install latest npm
|
||||
- name: Install latest npm (win)
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
# for some reason there's a bug in upgrading npm on win, have to install a specific version first
|
||||
# https://github.com/actions/setup-node/issues/623
|
||||
run: |
|
||||
npm install -g npm@8.3.1
|
||||
npm install -g npm@latest
|
||||
- name: Install latest npm (non-win)
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
run: npm install -g npm
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
@@ -70,7 +82,7 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
@@ -78,7 +90,3 @@ jobs:
|
||||
install-command: npm ci
|
||||
- name: ESLint
|
||||
run: npm run lint:ci
|
||||
- name: Validate Renovate Config
|
||||
uses: rinchsan/renovate-config-validator@1ea1e8514f6a33fdd71c40b0a5fa3512b9e7b936 # tag=v0
|
||||
with:
|
||||
pattern: '{.renovaterc.json,renovate/default.json}'
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
@@ -14,8 +14,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# node-version: [14.x, 16.x, 18.x]
|
||||
node-version: [16.x, 18.x]
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
||||
+15
-26
@@ -1,22 +1,6 @@
|
||||
name: Build Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
# this list of paths could potentially impact doc generation.
|
||||
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/**'
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/typedoc.json'
|
||||
@@ -31,17 +15,27 @@ on:
|
||||
- '.github/workflows/docs.yml' # this file
|
||||
- '!**/test/**'
|
||||
- '!**/sample-code/**'
|
||||
|
||||
branches: [master]
|
||||
|
||||
concurrency: preview-${{ github.ref }}
|
||||
push:
|
||||
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/**'
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Install dependencies (Node.js)
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
@@ -55,8 +49,3 @@ jobs:
|
||||
git config --local user.name "github-actions[bot]"
|
||||
- name: Build Docs
|
||||
run: npm run docs:preview
|
||||
- name: Deploy Preview
|
||||
uses: rossjrw/pr-preview-action@v1 # 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@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
with:
|
||||
ref: master
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -4,17 +4,12 @@ on:
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
branches: [master]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
label-check:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
checks: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danielchabr/pr-labels-checker@e1d7e79aa0933af1728011705a30528a279874fd # v3.1
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Validate Renovate Configs
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/renovate.yml' # this file
|
||||
- '.renovaterc.json'
|
||||
- 'renovate/default.json'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/renovate.yml' # this file
|
||||
- '.renovaterc.json'
|
||||
- 'renovate/default.json'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
name: Check Renovate Configs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Validate Renovate Config
|
||||
uses: rinchsan/renovate-config-validator@1ea1e8514f6a33fdd71c40b0a5fa3512b9e7b936 # tag=v0
|
||||
with:
|
||||
pattern: '{.renovaterc.json,renovate/default.json}'
|
||||
+1
-2
@@ -7,6 +7,5 @@ module.exports = {
|
||||
// forbids use of .only() in CI
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
color: true,
|
||||
// increase default timeout for CI since it can be slow
|
||||
timeout: process.env.CI ? '5s' : '2s',
|
||||
timeout: '5s',
|
||||
};
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"stateful.runme"
|
||||
]
|
||||
}
|
||||
+17
-5
@@ -17,9 +17,12 @@ 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',
|
||||
'./packages/*/lib/**/*.ts',
|
||||
'./packages/*/test/**/*helper*.js',
|
||||
'./packages/*/test/**/*helper*.ts',
|
||||
'./packages/*/test/**/*mock*.js',
|
||||
'./packages/*/test/**/*mock*.ts',
|
||||
'./packages/*/package.json',
|
||||
'./packages/*/tsconfig*.json',
|
||||
'./packages/*/test/**/fixture?(s)/**/*',
|
||||
@@ -43,12 +46,21 @@ module.exports = (wallaby) => {
|
||||
instrument: false,
|
||||
pattern: './packages/typedoc-plugin-appium/lib/theme/resources/**',
|
||||
},
|
||||
{
|
||||
instrument: false,
|
||||
pattern: './packages/*/test/**/__snapshots__/**/*',
|
||||
},
|
||||
'!./packages/*/test/**/*-specs.js',
|
||||
'!./packages/*/test/**/*.e2e.spec.(j|t)s',
|
||||
'!./packages/*/test/**/*.e2e.spec.js',
|
||||
'!./packages/*/test/**/*.e2e.spec.ts',
|
||||
'!**/local_appium_home/**',
|
||||
],
|
||||
testFramework: 'mocha',
|
||||
tests: ['./packages/*/test/unit/**/*.spec.(j|t)s', '!**/local_appium_home/**'],
|
||||
tests: [
|
||||
'./packages/*/test/unit/**/*.spec.js',
|
||||
'./packages/*/test/unit/**/*.spec.ts',
|
||||
'!**/local_appium_home/**',
|
||||
],
|
||||
workers: {
|
||||
// restart: true,
|
||||
},
|
||||
|
||||
+36
-3917
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,8 @@ The current committers are (in addition to the TC members above):
|
||||
* [@boneskull](https://github.com/boneskull)
|
||||
* [@mwakizaka](https://github.com/mwakizaka)
|
||||
* [@rerorero](https://github.com/rerorero)
|
||||
* [@tomriddly](https://github.com/tomriddly)
|
||||
* [@eglitise](https://github.com/eglitise)
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -13,16 +13,16 @@ 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 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.
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
|
||||
|
||||
:bangbang: Major documentation revision in progress
|
||||
|
||||
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x anymore, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.github.io/appium/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
|
||||
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x anymore, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
|
||||
|
||||
Appium is in the final stages of a major revision (to version 2.0). As such, the documentation
|
||||
found around the web may not be correct. The current Appium 2.0 documentation is very much in
|
||||
progress. Currently, it can be found [here](https://appium.github.io/appium/docs/en/latest/).
|
||||
progress. Currently, it can be found [here](https://appium.io/docs/en/latest/).
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -71,8 +71,8 @@ appium driver uninstall xcuitest
|
||||
```
|
||||
|
||||
You can find a 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 at
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers at
|
||||
the current Appium 2.0 documentation.
|
||||
|
||||
### Plugins
|
||||
@@ -104,8 +104,8 @@ appium server --use-plugins=device-farm,images
|
||||
```
|
||||
|
||||
You can find a full list of
|
||||
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#plugins) and
|
||||
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-plugins) plugins at
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#plugins) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-plugins) plugins at
|
||||
the current Appium 2.0 documentation.
|
||||
|
||||
|
||||
@@ -169,11 +169,11 @@ Java/Kotlin. Appium opens up the possibility of true cross-platform native app
|
||||
automation, for mobile and beyond. Finally!
|
||||
|
||||
If you're new to Appium or want a more comprehensive description of what this is all
|
||||
about, please read our [Intro to Appium](https://appium.github.io/appium/docs/en/latest/intro/).
|
||||
about, please read our [Intro to Appium](https://appium.io/docs/en/latest/intro/).
|
||||
|
||||
### Quickstart
|
||||
|
||||
Check out our [Quickstart](https://appium.github.io/appium/docs/en/latest/quickstart/) guide
|
||||
Check out our [Quickstart](https://appium.io/docs/en/latest/quickstart/) guide
|
||||
to get going with Appium.
|
||||
|
||||
There is also a sample code that contains [many examples of tests in a variety
|
||||
@@ -181,7 +181,7 @@ of different languages](https://github.com/appium/appium/tree/1.x/sample-code)!
|
||||
|
||||
### Documentation
|
||||
|
||||
For prettily-rendered docs, please visit [Appium Documentation](https://appium.github.io/appium).
|
||||
For prettily-rendered docs, please visit [Appium Documentation](https://appium.io).
|
||||
You can always find the full list of Appium doc pages at [Appium's GitHub
|
||||
Repo](https://github.com/appium/appium/tree/master/packages/appium/docs) as well.
|
||||
|
||||
@@ -196,7 +196,7 @@ Interested in where Appium is heading in the future? Check out the [Roadmap](ROA
|
||||
|
||||
### Project History, Credits & Inspiration
|
||||
|
||||
* [History](https://appium.github.io/appium/docs/en/latest/intro/history/)
|
||||
* [History](https://appium.io/docs/en/latest/intro/history/)
|
||||
|
||||
### User Forums
|
||||
|
||||
|
||||
+9
-8
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/lerna",
|
||||
"changelogPreset": "conventional-changelog-conventionalcommits",
|
||||
"command": {
|
||||
"add": {
|
||||
"exact": true
|
||||
},
|
||||
"publish": {
|
||||
"message": "chore: publish"
|
||||
},
|
||||
"run": {
|
||||
"concurrency": 8
|
||||
},
|
||||
@@ -10,16 +17,10 @@
|
||||
"version": {
|
||||
"allowBranch": "master"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
"exact": true
|
||||
},
|
||||
"publish": {
|
||||
"message": "chore: publish"
|
||||
}
|
||||
},
|
||||
"ignoreChanges": ["**/test/**", "**/*.md"],
|
||||
"version": "independent",
|
||||
"useNx": false,
|
||||
"useWorkspaces": true,
|
||||
"useNx": false
|
||||
"version": "independent"
|
||||
}
|
||||
|
||||
Generated
+19275
-2809
File diff suppressed because it is too large
Load Diff
+22
-19
@@ -34,7 +34,7 @@
|
||||
"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:distfiles": "npx rimraf --glob \"./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",
|
||||
@@ -85,31 +85,33 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"@commitlint/cli": "17.5.1",
|
||||
"@commitlint/config-conventional": "17.4.4",
|
||||
"@commitlint/cli": "17.6.5",
|
||||
"@commitlint/config-conventional": "17.6.5",
|
||||
"@tsconfig/node14": "1.0.3",
|
||||
"@types/chai": "4.3.4",
|
||||
"@types/chai": "4.3.5",
|
||||
"@types/chai-as-promised": "7.1.5",
|
||||
"@types/diff": "5.0.3",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/node": "18.16.16",
|
||||
"@types/semver": "7.5.0",
|
||||
"@types/sinon": "10.0.15",
|
||||
"@types/sinon-chai": "3.2.9",
|
||||
"@types/source-map-support": "0.5.6",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"@types/ws": "8.5.4",
|
||||
"@types/yargs": "17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||
"@typescript-eslint/parser": "5.59.8",
|
||||
"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.37.0",
|
||||
"eslint": "8.42.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-find-rules": "4.1.0",
|
||||
"eslint-import-resolver-typescript": "3.5.4",
|
||||
"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",
|
||||
@@ -118,20 +120,21 @@
|
||||
"get-port": "5.1.1",
|
||||
"handlebars": "4.7.7",
|
||||
"husky": "8.0.3",
|
||||
"json-schema-to-typescript": "12.0.0",
|
||||
"lerna": "6.6.1",
|
||||
"lint-staged": "13.2.0",
|
||||
"json-schema-to-typescript": "13.0.1",
|
||||
"lerna": "6.6.2",
|
||||
"lint-staged": "13.2.2",
|
||||
"log-symbols": "4.1.0",
|
||||
"midnight-smoker": "2.0.1",
|
||||
"midnight-smoker": "2.0.4",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
"mjpeg-server": "0.3.1",
|
||||
"mocha": "10.2.0",
|
||||
"mocha-chai-jest-snapshot": "1.1.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.8.7",
|
||||
"prettier": "2.8.8",
|
||||
"rewiremock": "3.14.5",
|
||||
"rimraf": "3.0.2",
|
||||
"rimraf": "5.0.1",
|
||||
"serve-static": "1.15.0",
|
||||
"sinon": "15.0.3",
|
||||
"sinon": "15.1.0",
|
||||
"sinon-chai": "3.7.0",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"through2": "4.0.2",
|
||||
@@ -142,7 +145,7 @@
|
||||
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
|
||||
"typescript": "4.9.5",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "7.30.2",
|
||||
"webdriverio": "7.31.1",
|
||||
"ws": "8.13.0",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,133 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.0-rc.2](https://github.com/appium/appium/compare/appium@2.0.0-rc.1...appium@2.0.0-rc.2) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-rc.1](https://github.com/appium/appium/compare/appium@2.0.0-beta.71...appium@2.0.0-rc.1) (2023-06-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Allow to provide ipv6 addresses and handle broadcast addresses in logs ([#18674](https://github.com/appium/appium/issues/18674)) ([8f63dab](https://github.com/appium/appium/commit/8f63dabb5dca882e522026f243e3445f5418874b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** update dependency winston to v3.9.0 ([a98d9c3](https://github.com/appium/appium/commit/a98d9c33c761af6870411721cc0d6c4491b43940))
|
||||
* **docutils:** update dependency yaml to v2.3.0 ([325d237](https://github.com/appium/appium/commit/325d2372078ee01e8fb58613ffd13417f2527151))
|
||||
* **docutils:** update dependency yaml to v2.3.1 ([1a2260b](https://github.com/appium/appium/commit/1a2260bb15d75e4f4c23551dc61529108a7b0d88))
|
||||
* ensure logServerAddress gets an actual URL ([#18754](https://github.com/appium/appium/issues/18754)) ([901ac71](https://github.com/appium/appium/commit/901ac711c4e2207f30d93ad177a332b7d6f23c38))
|
||||
* **support:** update dependency @types/semver to v7.5.0 ([528dcfe](https://github.com/appium/appium/commit/528dcfe324e75e48471cdb694448c2a09ec2d828))
|
||||
* **support:** update dependency semver to v7.5.1 ([5a55509](https://github.com/appium/appium/commit/5a555090c5322318f845415d743ee5c645fb94a0))
|
||||
* **types:** fix broken appium config schema types, temporarily ([fd2c72f](https://github.com/appium/appium/commit/fd2c72fd886ecaaf0f6588a328ff8a268a54fb55))
|
||||
* **types:** update dependency type-fest to v3.11.0 ([19277f6](https://github.com/appium/appium/commit/19277f6e14a56e52b4669d633e148ad4a3da2c7a))
|
||||
* **types:** update dependency type-fest to v3.11.1 ([56499eb](https://github.com/appium/appium/commit/56499eb997b551739bed628f057de7987674ea7f))
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.71](https://github.com/appium/appium/compare/appium@2.0.0-beta.70...appium@2.0.0-beta.71) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.70](https://github.com/appium/appium/compare/appium@2.0.0-beta.69...appium@2.0.0-beta.70) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.69](https://github.com/appium/appium/compare/appium@2.0.0-beta.68...appium@2.0.0-beta.69) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.68](https://github.com/appium/appium/compare/appium@2.0.0-beta.67...appium@2.0.0-beta.68) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.67](https://github.com/appium/appium/compare/appium@2.0.0-beta.66...appium@2.0.0-beta.67) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** update dependency yaml to v2.2.2 ([ddadd69](https://github.com/appium/appium/commit/ddadd6977d76a8a5ce1559d2a4ff4a31b58fb42f))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v7.31.1 ([6499eea](https://github.com/appium/appium/commit/6499eea0af6aea1bfb69b1e36c4c445e3ab05b82))
|
||||
* **support:** update dependency axios to v1.3.6 ([6692227](https://github.com/appium/appium/commit/66922279b7742a08613f472585a4a1cb70f80683))
|
||||
* **support:** update dependency axios to v1.4.0 ([91a6bc5](https://github.com/appium/appium/commit/91a6bc5925ab8ffc4ab6d05883900f7d186e49a9))
|
||||
* **support:** update dependency semver to v7.5.0 ([c568523](https://github.com/appium/appium/commit/c568523e017a8b52bdf5132a48bbfde791de704f))
|
||||
* **types:** update dependency type-fest to v3.10.0 ([3c4d3ac](https://github.com/appium/appium/commit/3c4d3acc09d2ca1ed74dc77c18c62482e4c70239))
|
||||
* **types:** update dependency type-fest to v3.9.0 ([94a207f](https://github.com/appium/appium/commit/94a207fc9718068f3657c51cc8be0ef682f16b11))
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.66](https://github.com/appium/appium/compare/appium@2.0.0-beta.65...appium@2.0.0-beta.66) (2023-04-20)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.65](https://github.com/appium/appium/compare/appium@2.0.0-beta.64...appium@2.0.0-beta.65) (2023-04-18)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.0-beta.64](https://github.com/appium/appium/compare/appium@2.0.0-beta.63...appium@2.0.0-beta.64) (2023-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency semver to v7.4.0 ([6ae86bd](https://github.com/appium/appium/commit/6ae86bd5c7be879a4d8852951d73eb2f68128df3))
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
+12
-12
@@ -13,16 +13,16 @@ 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 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.
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
|
||||
|
||||
:bangbang: Major documentation revision in progress
|
||||
|
||||
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x anymore, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.github.io/appium/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
|
||||
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x anymore, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
|
||||
|
||||
Appium is in the final stages of a major revision (to version 2.0). As such, the documentation
|
||||
found around the web may not be correct. The current Appium 2.0 documentation is very much in
|
||||
progress. Currently, it can be found [here](https://appium.github.io/appium/docs/en/latest/).
|
||||
progress. Currently, it can be found [here](https://appium.io/docs/en/latest/).
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -71,8 +71,8 @@ appium driver uninstall xcuitest
|
||||
```
|
||||
|
||||
You can find a 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 at
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers at
|
||||
the current Appium 2.0 documentation.
|
||||
|
||||
### Plugins
|
||||
@@ -104,8 +104,8 @@ appium server --use-plugins=device-farm,images
|
||||
```
|
||||
|
||||
You can find a full list of
|
||||
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#plugins) and
|
||||
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-plugins) plugins at
|
||||
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#plugins) and
|
||||
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-plugins) plugins at
|
||||
the current Appium 2.0 documentation.
|
||||
|
||||
|
||||
@@ -169,11 +169,11 @@ Java/Kotlin. Appium opens up the possibility of true cross-platform native app
|
||||
automation, for mobile and beyond. Finally!
|
||||
|
||||
If you're new to Appium or want a more comprehensive description of what this is all
|
||||
about, please read our [Intro to Appium](https://appium.github.io/appium/docs/en/latest/intro/).
|
||||
about, please read our [Intro to Appium](https://appium.io/docs/en/latest/intro/).
|
||||
|
||||
### Quickstart
|
||||
|
||||
Check out our [Quickstart](https://appium.github.io/appium/docs/en/latest/quickstart/) guide
|
||||
Check out our [Quickstart](https://appium.io/docs/en/latest/quickstart/) guide
|
||||
to get going with Appium.
|
||||
|
||||
There is also a sample code that contains [many examples of tests in a variety
|
||||
@@ -181,7 +181,7 @@ of different languages](https://github.com/appium/appium/tree/1.x/sample-code)!
|
||||
|
||||
### Documentation
|
||||
|
||||
For prettily-rendered docs, please visit [Appium Documentation](https://appium.github.io/appium).
|
||||
For prettily-rendered docs, please visit [Appium Documentation](https://appium.io).
|
||||
You can always find the full list of Appium doc pages at [Appium's GitHub
|
||||
Repo](https://github.com/appium/appium/tree/master/packages/appium/docs) as well.
|
||||
|
||||
@@ -196,7 +196,7 @@ Interested in where Appium is heading in the future? Check out the [Roadmap](ROA
|
||||
|
||||
### Project History, Credits & Inspiration
|
||||
|
||||
* [History](https://appium.github.io/appium/docs/en/latest/intro/history/)
|
||||
* [History](https://appium.io/docs/en/latest/intro/history/)
|
||||
|
||||
### User Forums
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ extra:
|
||||
link: https://twitter.com/AppiumDevs
|
||||
alternate:
|
||||
- name: English
|
||||
link: /appium/docs/en/
|
||||
link: /docs/en/
|
||||
lang: en
|
||||
- name: 日本
|
||||
link: /appium/docs/ja/
|
||||
link: /docs/ja/
|
||||
lang: ja
|
||||
- name: 中文简体
|
||||
link: /appium/docs/zh/
|
||||
link: /docs/zh/
|
||||
lang: zh
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Contributing to Appium
|
||||
cwd: ../../../../../
|
||||
---
|
||||
|
||||
This section of Appium's documentation is for those who are interested in contributing to Appium's
|
||||
@@ -16,8 +17,105 @@ already.
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
TODO
|
||||
The project offers a variety of ways to contribute, e.g.:
|
||||
|
||||
- contributing code
|
||||
- improving documentation
|
||||
- creating educational content (blog posts, tutorials, videos, etc.)
|
||||
- spreading the good word about the project (e.g. via Twitter)
|
||||
- filing bugs if you discover them while using Appium
|
||||
- making feature requests if you are missing something in the project or help triaging bugs
|
||||
- supporting users in the [Appium forum](https://discuss.appium.io/)
|
||||
|
||||
## The Appium Development Process
|
||||
|
||||
TODO
|
||||
To contribute to the Appium code base make sure to check out the Git repository.
|
||||
|
||||
!!! info
|
||||
|
||||
If you are VS Code user you can easily check out the project using [Runme](https://runme.dev/api/runme?repository=https%3A%2F%2Fgithub.com%2Fappium%2Fappium.git&fileToOpen=packages%2Fappium%2Fdocs%2Fen%2Fcontributing%2Findex.md).
|
||||
|
||||
It is advised to [fork](https://github.com/appium/appium/fork) before cloning it to your system.
|
||||
|
||||
```sh { name=clone }
|
||||
export GITHUB_USERNAME=<your-username>
|
||||
git clone git@github.com:$GITHUB_USERNAME/appium.git
|
||||
```
|
||||
|
||||
After cloning you can go ahead and install the project dependencies:
|
||||
|
||||
```sh { name=install }
|
||||
npm install
|
||||
```
|
||||
|
||||
From here on there are several things you can do.
|
||||
|
||||
### Watch Files
|
||||
|
||||
When developing Appium code we have to watch all JavaScript and TypeScript files to re-compile them after every change. You can run this watch process via:
|
||||
|
||||
```sh { name=watch, background=true }
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Start Appium in Dev-Mode
|
||||
|
||||
To test your changes you can run Appium in dev mode via:
|
||||
|
||||
```sh { name=start }
|
||||
npm start
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
The project maintains a set of different test variations you can run to verify the quality of the code.
|
||||
|
||||
#### Linting
|
||||
|
||||
Appium uses EsLint for static code analysis and linting. You can run these checks via:
|
||||
|
||||
```sh { name=test-linting }
|
||||
npm run lint
|
||||
```
|
||||
|
||||
#### Unit
|
||||
|
||||
Run via:
|
||||
|
||||
```sh { name=test-unit }
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
You can also run tests for specific workspaces, e.g.:
|
||||
|
||||
```sh { name=test-workspace }
|
||||
export APPIUM_WORKSPACE=@appium/doctor
|
||||
npm run test:unit -w $APPIUM_WORKSPACE
|
||||
```
|
||||
|
||||
#### Smoke and e2e Tests
|
||||
|
||||
Run via:
|
||||
|
||||
```sh { name=test-slow }
|
||||
npm run test:slow
|
||||
```
|
||||
|
||||
### Deploy Docs Locally
|
||||
|
||||
Our documentation system uses [MKDocs](https://www.mkdocs.org/) and therefor requires [Python](https://www.python.org/) to be installed on your system. You can run the docs by:
|
||||
|
||||
```sh
|
||||
# installing needed Python dependencies
|
||||
pip install -r packages/docutils/requirements.txt
|
||||
# build the project
|
||||
npm run build
|
||||
# run dev server
|
||||
npm run dev:docs
|
||||
```
|
||||
|
||||
You should be able to view the page at `http://127.0.0.1:8000/docs/en`:
|
||||
|
||||
```sh
|
||||
open http://127.0.0.1:8000/docs/en
|
||||
```
|
||||
|
||||
@@ -2,4 +2,241 @@
|
||||
title: Building Docs for Appium Extensions
|
||||
---
|
||||
|
||||
TODO
|
||||
Once you've [built a driver](./build-drivers.md) or [built a plugin](./build-plugins.md) for Appium,
|
||||
you will hopefully want to document how that extension works for your users. The most basic way of
|
||||
doing this is to write up a quick `README.md` and keep it in the root of your project's repository.
|
||||
However, this can involve a lot of duplication of effort, especially when documenting things like
|
||||
Appium commands.
|
||||
|
||||
Let's say your driver implements ~25 of the standard WebDriver protocol commands. You could write
|
||||
up a description of these commands, how they map to the protocol, what parameters they take, and
|
||||
what behaviour will result on your particular platform. But this information is already more or
|
||||
less stored in your code, as the command implementation (and any docstrings or comments). Having
|
||||
this information in two places creates an opportunity for the docs to get out of sync with the
|
||||
reality of the code. Wouldn't it be nice to generate command reference documentation straight from
|
||||
the code?
|
||||
|
||||
Another problem with the basic single file `README.md` approach is that many extensions might want a
|
||||
whole set of documents including longer prose guides (like this one). It might be nice to have code
|
||||
examples where you can toggle between different programming languages. It might be nice to be able
|
||||
to add a project-specific logo. And so on.
|
||||
|
||||
The Appium project has built tools to do all these things, and we've packaged up these tools so our
|
||||
ecosystem developers building drivers and plugins can _also_ use them. The best way to get going
|
||||
with these tools is probably to look at an existing Appium driver repo to see how it's done, for
|
||||
example the [XCUITest driver repo](https://github.com/appium/appium-xcuitest-driver). But this guide
|
||||
will outline the basic approach.
|
||||
|
||||
### Conceptual architecture
|
||||
|
||||
Appium settled on [MkDocs](https://www.mkdocs.org/) as a Markdown-based documentation site
|
||||
generator. It uses a Python toolchain (and not Node.js), but it turned out to be the best option
|
||||
for our purposes. You can adjust this, but by default Appium's utilities also assume that you'll be
|
||||
using the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) theme/extension for
|
||||
MkDocs.
|
||||
|
||||
From here, building a basic docs site is as easy as collecting your Markdown files together and
|
||||
creating a sort of manifest file defining how you want them to be organized.
|
||||
|
||||
The other main piece is automatic documentation generation from your code files. Appium maintains
|
||||
a plugin for [TypeDoc](https://typedoc.org/). This plugin is incorporated into our doc utility.
|
||||
When you give it an entrypoint for you driver or plugin, it will scan and parse all your code files
|
||||
looking for Appium command implementations. A set of Markdown reference files will be generated for these
|
||||
commands, which will then be included in your docs site.
|
||||
|
||||
> Note: Implementing an extension in TypeScript is _not_ a requirement for generating documentation,
|
||||
> but for automated doc generation to work, you will need to apply TypeScript-supported JSDoc-style
|
||||
> docstrings to your JS codebase. See ["JS Projects Utilizing TypeScript"](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html) for more information.
|
||||
|
||||
In order to make different versions of your docs available (one for each minor release of your
|
||||
extension, typically), we also bundle [Mike](https://github.com/jimporter/mike).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To take advantage of Appium's documentation utilities, you'll need to install:
|
||||
|
||||
- [Python v3+](https://www.python.org/downloads/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installation/) (this may be installed automatically with Python)
|
||||
- The `@appium/docutils` package:
|
||||
|
||||
```bash
|
||||
npm install --save-dev @appium/docutils
|
||||
```
|
||||
|
||||
### Initializing an Extension for Building Docs
|
||||
|
||||
To prepare your extension for generating documentation, run the following command:
|
||||
|
||||
```bash
|
||||
npx appium-docs init --entry-point <my-entry-point.js>
|
||||
```
|
||||
|
||||
...where `<my-entry-point.js>` is the **source entry point** to your extension. If you _are not_ transpiling your code via TypeScript, Babel, etc., this is typically the same as the value of the `main` property in `package.json`. If you _are_ transpiling, this is typically different. For example, your `main` property may be `dist/index.js`, but your **source entry point** is `src/index.ts`.
|
||||
|
||||
This will:
|
||||
|
||||
1. Create a `tsconfig.json` if one does not already exist. This is necessary even if your extension is not written in TypeScript.
|
||||
2. Create a `typedoc.json` with the necessary configuration for TypeDoc.
|
||||
3. Create a `mkdocs.yml` with the necessary configuration for MkDocs.
|
||||
4. Modify your `package.json` to add a `typedoc.entryPoint` property with a value of your entry point (as specified above).
|
||||
|
||||
### Documenting Your Extension
|
||||
|
||||
At this point, you can begin documenting your extension. You don't need to do this all at once, but you should make the following changes, at minimum.
|
||||
|
||||
#### `newMethodMap` and `executeMethodMap`
|
||||
|
||||
The static properties `newMethodMap` and `executeMethodMap` may be present on your extension's main class. If they are not, then you can skip to the next section. If they are, you will need to make the following changes, depending on your extension's language.
|
||||
|
||||
##### JavaScript
|
||||
|
||||
```js
|
||||
// note: this is equivalent to the TypeScript example below
|
||||
|
||||
class MyExtension {
|
||||
static newMethodMap = /** @type {const} */({
|
||||
// ...
|
||||
});
|
||||
|
||||
static executeMethodMap = /** @type {const} */({
|
||||
// ...
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
##### TypeScript
|
||||
|
||||
```ts
|
||||
class MyExtension {
|
||||
static newMethodMap = {
|
||||
// ...
|
||||
} as const;
|
||||
|
||||
static executeMethodMap = {
|
||||
// ...
|
||||
} as const;
|
||||
}
|
||||
```
|
||||
|
||||
#### Driver Constraints
|
||||
|
||||
> Note: Plugin authors can skip this section.
|
||||
|
||||
Your driver may have a property `desiredCapConstraints`. It should also follow the same pattern as `newMethodMap` and `executeMethodMap` above. For example:
|
||||
|
||||
```js
|
||||
class MyExtension {
|
||||
desiredCapConstraints = /** @type {const} */({
|
||||
myCapability: {
|
||||
presence: true,
|
||||
isString: true
|
||||
},
|
||||
myOtherCap: {
|
||||
isBoolean: true
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
(For extensions written in TypeScript, use `as const` as before.)
|
||||
|
||||
#### Commands
|
||||
|
||||
The documentation for a command, as defined in your extension, comes from multiple places. These sources are then combined as needed into the final output.
|
||||
|
||||
In Appium, new commands are defined in `newMethodMap` and execute methods are defined in `executeMethodMap`. **The value of these properties are used to build your documentation.** In particular, parameter names and optional/required status _override_ whatever method implementation does. So for example, if your `newMethodMap` contains:
|
||||
|
||||
```js
|
||||
class MyExtension {
|
||||
static newMethodMap = /** @type {const} */({
|
||||
'/session/:sessionId/myThing': {
|
||||
/**
|
||||
* Does my thing
|
||||
*/
|
||||
GET: {command: 'doMyThing', payloadParams: {required: ['a', 'b']}},
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
...and your `doMyThing` method implementation looks like this:
|
||||
|
||||
```js
|
||||
class MyExtension {
|
||||
/**
|
||||
* Doesn't do my thing
|
||||
* @param {any} a - Whatever
|
||||
* @param {number} d - Some number
|
||||
* @param {boolean} c - Some boolean
|
||||
* @returns {Promise<boolean>} Some other boolean
|
||||
*/
|
||||
async doMyThing(a, d = 1, c = false) {
|
||||
// ...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The documentation will show that the `doMyThing` method accepts required parameters `a` and `b`. Even though `b` is named `d` in the implementation--and it's optional--it will be ignored. Likewise, since the definition in `newMethodMap` knows nothing about `c`, it too is ignored.
|
||||
|
||||
In addition, the _description_ from the docstring in `newMethodMap` overrides the description in the method implementation; it will describe the command `doMyThing` as "Does my thing".
|
||||
|
||||
The `@param` tags and `@returns` tag from the method's docstring provide information about the expected and returned types, as well as a description of each. This is not expressible via `newMethodMap` and `executeMethodMap` alone; it provides more information for your extension's users.
|
||||
|
||||
> Note for TypeScript users: while the _types_ will already be present, providing `@param` and `@returns` tags is still useful for providing descriptions.
|
||||
|
||||
All commands must be `async`, so they will return `Promise<T>` where type `T` is the type of whatever the `Promise` fulfills with. In the generated documentation, the `Promise` is ignored, and only `T` is reported. So for `doMyThing`, the return type of the command, as output in the documentation, will be `boolean`.
|
||||
|
||||
This is because while extensions must be written in JavaScript, we're documenting an API which can be called from any language; that language likely won't have a concept of a `Promise`. Likewise, `undefined` or `void` types will be output as `null` (since that is a concept that translates well to multiple languages).
|
||||
|
||||
#### Optional: `README.md`
|
||||
|
||||
If you have a `README.md`, it will be pulled in to the generated docs site automatically. This behavior can be disabled by adding the following to `typedoc.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"readme": "none"
|
||||
}
|
||||
```
|
||||
|
||||
#### Next Steps
|
||||
|
||||
Appium provides type definitions for extension authors; these are available via Appium itself and the [`@appium/types`](https://npm.im/@appium/types) package.
|
||||
|
||||
Of note, drivers should both extend `BaseDriver` _and_ implement the `ExternalDriver` interface exported by `@appium/types`. This will help ensure that your driver's implementation is correct and usable by different clients.
|
||||
|
||||
You're encouraged to look at the official test/example extensions, [`@appium/fake-driver`](https://npm.im/@appium/fake-driver) and [`@appium/fake-plugin`](https://npm.im/@appium/fake-plugin) for canonical examples of how to use these types.
|
||||
|
||||
#### Resources
|
||||
|
||||
How to type more complex return values or parameters is beyond the scope of this document. For more information, see:
|
||||
|
||||
- For extensions written in JS, the [TypeScript documentation](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html)
|
||||
- For tags in addition to what TS natively recognizes, see the [TypeDoc documentation](https://typedoc.org/guides/doccomments/)
|
||||
|
||||
Likewise, refer to the [MkDocs documentation](https://www.mkdocs.org/user-guide/writing-your-docs/) for further information on how to customize your MkDocs output.
|
||||
|
||||
### Usage
|
||||
|
||||
At this point, you can use the `appium-docs` CLI tool. Run this tool with no arguments to get the
|
||||
full help output and see all the available subcommands and parameters. Here are a few usage
|
||||
examples:
|
||||
|
||||
```bash
|
||||
# Generate reference and build the mkdocs site into the site dir
|
||||
npx appium-docs build
|
||||
|
||||
# Same as build, but host the docs on a local dev server
|
||||
# and watch for changes and rebuild when files change
|
||||
npx appium-docs build --serve
|
||||
|
||||
# Build the docs and deploy them with mike versioning to the docs-site branch
|
||||
# using the included commit message and rebase strategy on the branch.
|
||||
# This is particularly useful for pushing content to a GitHub pages branch!
|
||||
npx appium-docs build \
|
||||
--deploy \
|
||||
-b docs-site \
|
||||
-m 'docs: auto-build docs for appium-xcuitest-driver@%s' \
|
||||
--rebase
|
||||
```
|
||||
|
||||
@@ -64,7 +64,7 @@ the Appium client depends on) since that is what you will use as your primary in
|
||||
|[Appium Java client](https://github.com/appium/java-client)|Java|Appium Team|
|
||||
|[Appium Python client](https://github.com/appium/python-client)|Python|Appium Team|
|
||||
|[Appium Ruby Core client](https://github.com/appium/ruby_lib_core) (Recommended)<br>[Appium Ruby client](https://github.com/appium/ruby_lib)|Ruby|Appium Team|
|
||||
|[WebDriverIO](https://webdriver.io)|Node.js|Community|
|
||||
|[WebDriverIO](https://webdriver.io)<br>[Nightwatch](https://nightwatchjs.org/)|Node.js|Community|
|
||||
|[Appium .NET client](https://github.com/appium/dotnet-client)|C#|Appium Team*|
|
||||
|[RobotFramework](https://github.com/serhatbolsu/robotframework-appiumlibrary)|DSL|Community|
|
||||
|
||||
@@ -97,3 +97,11 @@ are currently maintained by the Appium Team:
|
||||
If you maintain an Appium plugin that you would like to be listed in the Appium docs, feel free
|
||||
to make a PR to add it to this section with a link to the documentation for the plugin.
|
||||
|
||||
## Helper tools
|
||||
|
||||
The following tools might also be useful for Appium users:
|
||||
|
||||
|Name|Description|Supported By|
|
||||
|---|---|---|
|
||||
|[appium-installer](https://github.com/AppiumTestDistribution/appium-installer)|Help set up an Appium environment for Android and iOS|`@AppiumTestDistribution`|
|
||||
|
||||
|
||||
@@ -80,7 +80,20 @@ APPIUM_SKIP_CHROMEDRIVER_INSTALL=1 appium driver install uiautomator2
|
||||
|
||||
With Appium 1.x, command-line options specific to particular drivers were all hosted on the main Appium server. So, for example, `--chromedriver-executable` was a CLI parameter you could use with Appium to set the location of a specific Chromedriver version for use with, say, the UiAutomator2 driver.
|
||||
|
||||
With Appium 2.x, all driver- and platform-specific CLI params have been moved to the drivers themselves. To access them, you'll now need to prepend the argument with the extension type (either `driver` or `plugin`) and the name of the extension. For example, `--chromedriver-executable` becomes `--driver-uiautomator2-chromedriver-executable`.
|
||||
With Appium 2.x, all driver- and platform-specific CLI params have been moved to the drivers themselves. To access the corresponding functionality, you'll need to refer to the driver/plugin documentation. In some cases, the extension will continue to expose CLI parameters. For example, the XCUITest driver used to expose a parameter `--webdriveragent-port`. Now, to access this parameter, it should be prefixed with `driver-xcuitest`, to differentiate it from parameters other drivers might also expose. To use this parameter, you thus need to start Appium with something like:
|
||||
|
||||
```bash
|
||||
appium --driver-xcuitest-webdriveragent-port=5000
|
||||
```
|
||||
|
||||
Some drivers have done away with CLI args entirely in favour of default capabilities instead. With
|
||||
the above-mentioned `--chromedriver-executable` param for example, you now need to take advantage
|
||||
of the `appium:chromedriverExecutable` capability supported by the UiAutomator2 driver. To set this
|
||||
capability from the command line, do the following:
|
||||
|
||||
```bash
|
||||
appium --default-capabilities '{"appium:chromedriverExecutable": "/path/to/chromedriver"}'
|
||||
```
|
||||
|
||||
### :bangbang: Driver-specific automation commands
|
||||
|
||||
@@ -231,10 +244,6 @@ any Appium command, or even adjust the way the underlying Appium HTTP server its
|
||||
more about plugins, read the new [Appium Introduction](../intro/index.md). Interested in building
|
||||
a plugin? Check out the [Building Plugins](../ecosystem/build-plugins.md) guide.
|
||||
|
||||
#### :tada: _Client Plugins_
|
||||
|
||||
TODO
|
||||
|
||||
### :tada: Install drivers and plugins from anywhere
|
||||
|
||||
You're no longer limited to the drivers that come with Appium, or that the Appium team even knows
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
INHERIT: ./base-mkdocs.yml
|
||||
site_url: https://appium.github.io/appium/docs/en
|
||||
site_url: https://appium.io/docs/en
|
||||
edit_uri: edit/master/packages/appium/docs/en
|
||||
site_description: The Appium automation project documentation
|
||||
docs_dir: en
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
INHERIT: ./base-mkdocs.yml
|
||||
site_url: https://appium.github.io/appium/docs/ja
|
||||
site_url: https://appium.io/docs/ja
|
||||
edit_uri: edit/master/packages/appium/docs/ja
|
||||
site_description: The Appium automation project documentation
|
||||
docs_dir: ja
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
INHERIT: ./base-mkdocs.yml
|
||||
site_url: https://appium.github.io/appium/docs/zh
|
||||
site_url: https://appium.io/docs/zh
|
||||
edit_uri: edit/master/packages/appium/docs/zh
|
||||
site_description: The Appium automation project documentation
|
||||
docs_dir: zh
|
||||
|
||||
@@ -31,7 +31,6 @@ 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;
|
||||
|
||||
async function main() {
|
||||
log.info(`Building Appium docs and committing to ${DOCS_BRANCH}`);
|
||||
@@ -66,7 +65,7 @@ async function main() {
|
||||
deployVersion: majMinVer,
|
||||
push,
|
||||
alias: LATEST_ALIAS,
|
||||
rebase,
|
||||
rebase: true, // we always want to rebase if branch is out of sync
|
||||
message: `docs(appium): auto-build docs for appium@${majMinVer}, language ${lang}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,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
|
||||
*/
|
||||
|
||||
@@ -424,7 +424,7 @@ class ExtensionCliCommand {
|
||||
* @param {ExtensionArgs} args
|
||||
* @returns {string}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getPostInstallText(args) {
|
||||
throw this._createFatalError('Must be implemented in final class');
|
||||
}
|
||||
@@ -505,7 +505,7 @@ class ExtensionCliCommand {
|
||||
* @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
|
||||
* @param {string} installSpec - Extension name/spec
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
validateExtensionFields(extMetadata, installSpec) {
|
||||
throw this._createFatalError('Must be implemented in final class');
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export class ExtensionConfig {
|
||||
* @param {ExtName<ExtType>} extName
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars,require-await
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getConfigWarnings(extManifest, extName) {
|
||||
return [];
|
||||
}
|
||||
@@ -421,7 +421,7 @@ export class ExtensionConfig {
|
||||
* @param {ExtName<ExtType>} extName - Extension name (from manifest)
|
||||
* @returns {ExtManifestProblem[]}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getGenericConfigProblems(extManifest, extName) {
|
||||
const {version, pkgName, mainClass} = extManifest;
|
||||
const problems = [];
|
||||
@@ -456,7 +456,7 @@ export class ExtensionConfig {
|
||||
* @param {ExtName<ExtType>} extName
|
||||
* @returns {ExtManifestProblem[]}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getConfigProblems(extManifest, extName) {
|
||||
// shoud override this method if special validation is necessary for this extension type
|
||||
return [];
|
||||
@@ -509,7 +509,7 @@ export class ExtensionConfig {
|
||||
* @param {ExtName<ExtType>[]} [activeNames]
|
||||
* @returns {void}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
print(activeNames) {
|
||||
if (_.isEmpty(this.installedExtensions)) {
|
||||
log.info(
|
||||
@@ -534,7 +534,7 @@ export class ExtensionConfig {
|
||||
* @returns {string}
|
||||
* @abstract
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
extensionDesc(extName, extManifest) {
|
||||
throw new Error('This must be implemented in a subclass');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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
|
||||
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';
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
isExtensionCommandArgs,
|
||||
isPluginCommandArgs,
|
||||
isServerCommandArgs,
|
||||
fetchIpAddresses,
|
||||
V4_BROADCAST_IP,
|
||||
V6_BROADCAST_IP,
|
||||
} from './utils';
|
||||
import os from 'node:os';
|
||||
|
||||
const {resolveAppiumHome} = env;
|
||||
|
||||
@@ -138,6 +142,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.
|
||||
*
|
||||
@@ -157,6 +207,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();
|
||||
|
||||
@@ -263,6 +320,28 @@ async function init(args) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the actual server address and the list of URLs that
|
||||
* could be used to connect to the current server.
|
||||
* Properly replaces broadcast addresses in client URLs.
|
||||
*
|
||||
* @param {string} url The URL the server is listening on
|
||||
*/
|
||||
function logServerAddress(url) {
|
||||
logger.info(`Appium REST http interface listener started on ${url}`);
|
||||
const urlObj = new URL(url);
|
||||
if (![V4_BROADCAST_IP, V6_BROADCAST_IP].includes(urlObj.hostname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ips = fetchIpAddresses(urlObj.hostname === V4_BROADCAST_IP ? 4 : 6);
|
||||
logger.info(
|
||||
`You can provide the following ${util.pluralize('URL', ips.length, false)} ` +
|
||||
`in your client code to connect to this server:\n` +
|
||||
ips.map((x) => `\t${urlObj.href.replace(urlObj.hostname, x)}`).join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Appium's config. Starts server if appropriate and resolves the
|
||||
* server instance if so; otherwise resolves w/ `undefined`.
|
||||
@@ -363,9 +442,7 @@ async function main(args) {
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Appium REST http interface listener started on ${parsedArgs.address}:${parsedArgs.port}${parsedArgs.basePath}`
|
||||
);
|
||||
logServerAddress(`http://${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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ export class RoachHotelMap extends Map {
|
||||
/**
|
||||
* @param {K} key
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
delete(key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import {inspect as dump} from 'util';
|
||||
import {node} from '@appium/support';
|
||||
import path from 'path';
|
||||
import {SERVER_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE} from './constants';
|
||||
import os from 'node:os';
|
||||
|
||||
const W3C_APPIUM_PREFIX = 'appium';
|
||||
const STANDARD_CAPS_LOWERCASE = new Set([...STANDARD_CAPS].map((cap) => cap.toLowerCase()));
|
||||
export const V4_BROADCAST_IP = '0.0.0.0';
|
||||
export const V6_BROADCAST_IP = '::';
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -331,6 +335,28 @@ export function isPluginCommandArgs(args) {
|
||||
return args.subcommand === PLUGIN_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of IP addresses of the current host.
|
||||
*
|
||||
* @param {4|6|null} family Either 4 to include ipv4 addresses only,
|
||||
* 6 to include ipv6 addresses only, or null to include all of them
|
||||
* @returns {string[]} The list of matched IP addresses
|
||||
*/
|
||||
export function fetchIpAddresses (family = null) {
|
||||
let familyValue = null;
|
||||
// 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6
|
||||
if (family === 4) {
|
||||
familyValue = [4, 'IPv4'];
|
||||
} else if (family === 6) {
|
||||
familyValue = [6, 'IPv6'];
|
||||
}
|
||||
return _.flatMap(_.values(os.networkInterfaces()).filter(Boolean))
|
||||
// @ts-ignore The linter does not understand the above filter
|
||||
.filter(({family}) => !familyValue || familyValue && familyValue.includes(family))
|
||||
// @ts-ignore The linter does not understand the above filter
|
||||
.map(({address}) => address);
|
||||
}
|
||||
|
||||
export {
|
||||
inspect,
|
||||
parseCapsForInnerDriver,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.0.0-beta.61",
|
||||
"version": "2.0.0-rc.2",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -60,17 +60,17 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.3.5",
|
||||
"@appium/base-plugin": "^2.2.5",
|
||||
"@appium/docutils": "^0.3.4",
|
||||
"@appium/schema": "^0.2.6",
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/types": "^0.10.3",
|
||||
"@appium/base-driver": "^9.3.12",
|
||||
"@appium/base-plugin": "^2.2.12",
|
||||
"@appium/docutils": "^0.4.1",
|
||||
"@appium/schema": "^0.3.0",
|
||||
"@appium/support": "^4.0.3",
|
||||
"@appium/types": "^0.12.0",
|
||||
"@sidvind/better-ajv-errors": "2.1.0",
|
||||
"@types/argparse": "2.0.10",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/fancy-log": "2.0.0",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/semver": "7.5.0",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"ajv": "8.12.0",
|
||||
@@ -78,7 +78,7 @@
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.0",
|
||||
"asyncbox": "2.9.4",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.4.0",
|
||||
"bluebird": "3.7.2",
|
||||
"cross-env": "7.0.3",
|
||||
"find-up": "5.0.0",
|
||||
@@ -89,13 +89,13 @@
|
||||
"ora": "5.4.1",
|
||||
"package-changed": "3.0.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.5.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.7.2",
|
||||
"winston": "3.8.2",
|
||||
"type-fest": "3.11.1",
|
||||
"winston": "3.9.0",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.2.1"
|
||||
"yaml": "2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "7.30.2"
|
||||
"webdriverio": "7.31.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import {exec} from 'teen_process';
|
||||
import {fs, tempDir, util} from '@appium/support';
|
||||
import {fs, system, tempDir, util} from '@appium/support';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {
|
||||
@@ -24,6 +24,8 @@ const TEST_DRIVER_INVALID_PEERS_DIR = path.dirname(
|
||||
);
|
||||
|
||||
describe('Driver CLI', function () {
|
||||
this.timeout(90000); // some of these tests involve network and can be very slow
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@@ -96,6 +98,10 @@ describe('Driver CLI', function () {
|
||||
});
|
||||
|
||||
it('should show updates for installed drivers with --updates', async function () {
|
||||
if (system.isWindows()) {
|
||||
// TODO figure out why this isn't working on windows
|
||||
return this.skip();
|
||||
}
|
||||
const versions = /** @type {string[]} */ (
|
||||
JSON.parse(
|
||||
(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {finalizeSchema, registerSchema, resetSchema} from '../../lib/schema/sche
|
||||
import extSchema from '../fixtures/driver-schema';
|
||||
import {resolveFixture} from '../helpers';
|
||||
import _ from 'lodash';
|
||||
import {system} from '@appium/support';
|
||||
|
||||
const resolveConfigFixture = _.partial(resolveFixture, 'config');
|
||||
|
||||
@@ -314,6 +315,10 @@ describe('config file behavior', function () {
|
||||
|
||||
describe('when the config file is invalid JSON', function () {
|
||||
it('should reject with a user-friendly error message', async function () {
|
||||
if (system.isWindows()) {
|
||||
// TODO figure out why this isn't working on windows
|
||||
return this.skip();
|
||||
}
|
||||
await readConfigFile(INVALID_JSON_FILEPATH).should.be.rejectedWith(
|
||||
new RegExp(`${INVALID_JSON_FILEPATH} is invalid`)
|
||||
);
|
||||
|
||||
+1
-3
@@ -11,11 +11,9 @@ export default [
|
||||
ref: 'appium.json#/properties/server/properties/address',
|
||||
},
|
||||
schema: {
|
||||
$comment: 'I think hostname covers both DNS and IPv4...could be wrong',
|
||||
appiumCliAliases: ['a'],
|
||||
default: '0.0.0.0',
|
||||
description: 'IP address to listen on',
|
||||
format: 'hostname',
|
||||
description: 'IPv4/IPv6 address or a hostname to listen on',
|
||||
title: 'address config',
|
||||
type: 'string',
|
||||
},
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('ExtensionConfig', function () {
|
||||
it('should throw', function () {
|
||||
expect(() => config.require('fake')).to.throw(
|
||||
ReferenceError,
|
||||
/^could not find a driver installed at \/some\/path\/node_modules\/flotsam/i
|
||||
/^could not find a driver installed at .+flotsam/i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,9 @@ describe('parser', function () {
|
||||
}).should.throw(/unrecognized arguments: --apple/i);
|
||||
});
|
||||
|
||||
it('should throw an error for an invalid value ("hostname")', function () {
|
||||
// FIXME: this test will not work until we restore the formatting restriction to the address validation
|
||||
// see #18716
|
||||
it.skip('should throw an error for an invalid value ("hostname")', function () {
|
||||
(() => {
|
||||
p.parseArgs(['--address', '-42']);
|
||||
}).should.throw(/must match format "hostname"/i);
|
||||
@@ -119,6 +121,18 @@ describe('parser', function () {
|
||||
p.parseArgs(['--allow-insecure', 'foo ,bar']).allowInsecure.should.eql(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should parse --address correctly', function () {
|
||||
p.parseArgs(['--address', 'localhost']).address.should.eql('localhost');
|
||||
p.parseArgs(['--address', 'appium.net']).address.should.eql('appium.net');
|
||||
p.parseArgs(['--address', '127.0.0.1']).address.should.eql('127.0.0.1');
|
||||
p.parseArgs(['--address', '10.0.0.1']).address.should.eql('10.0.0.1');
|
||||
p.parseArgs(['--address', '::']).address.should.eql('::');
|
||||
p.parseArgs(['--address', '::1']).address.should.eql('::1');
|
||||
p.parseArgs(['--address', '2a02:8888:9a80:158:2418:a474:43c6:1b78']).address.should.eql(
|
||||
'2a02:8888:9a80:158:2418:a474:43c6:1b78'
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse --deny-insecure correctly', function () {
|
||||
p.parseArgs([]).should.not.have.property('denyInsecure');
|
||||
p.parseArgs(['--deny-insecure', '']).denyInsecure.should.eql([]);
|
||||
@@ -264,6 +278,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,6 +5,7 @@ import {
|
||||
removeAppiumPrefixes,
|
||||
inspect,
|
||||
adjustNodePath,
|
||||
fetchIpAddresses,
|
||||
} from '../../lib/utils';
|
||||
import {BASE_CAPS, W3C_CAPS} from '../helpers';
|
||||
import _ from 'lodash';
|
||||
@@ -275,4 +276,19 @@ describe('utils', function () {
|
||||
(await fs.exists(process.env.NODE_PATH)).should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchIpAddresses()', function () {
|
||||
it('should fetch addresses for ipv4 only', async function () {
|
||||
fetchIpAddresses(4).length.should.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should fetch addresses for ipv6 only', async function () {
|
||||
fetchIpAddresses(6).length.should.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should fetch addresses for ipv4 and ipv6', async function () {
|
||||
fetchIpAddresses().length.should.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -3,6 +3,76 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [9.3.12](https://github.com/appium/appium/compare/@appium/base-driver@9.3.11...@appium/base-driver@9.3.12) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-driver
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [9.3.11](https://github.com/appium/appium/compare/@appium/base-driver@9.3.10...@appium/base-driver@9.3.11) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **schema:** update definitelytyped ([e967240](https://github.com/appium/appium/commit/e96724077ef2b5b8aae203856195f3bb8de56116))
|
||||
* **types:** update dependency type-fest to v3.11.0 ([19277f6](https://github.com/appium/appium/commit/19277f6e14a56e52b4669d633e148ad4a3da2c7a))
|
||||
* **types:** update dependency type-fest to v3.11.1 ([56499eb](https://github.com/appium/appium/commit/56499eb997b551739bed628f057de7987674ea7f))
|
||||
|
||||
|
||||
|
||||
## [9.3.10](https://github.com/appium/appium/compare/@appium/base-driver@9.3.9...@appium/base-driver@9.3.10) (2023-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** add missing @types/lodash ([63a429c](https://github.com/appium/appium/commit/63a429c53bf3da353e018ed86ec24e32bf238d18))
|
||||
|
||||
|
||||
|
||||
## [9.3.9](https://github.com/appium/appium/compare/@appium/base-driver@9.3.8...@appium/base-driver@9.3.9) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-driver
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [9.3.8](https://github.com/appium/appium/compare/@appium/base-driver@9.3.7...@appium/base-driver@9.3.8) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Ignore unknown script arguments ([#18575](https://github.com/appium/appium/issues/18575)) ([4f564ef](https://github.com/appium/appium/commit/4f564ef7ad350831b90994ffd4e8b00de227577d))
|
||||
* **support:** update dependency axios to v1.3.6 ([6692227](https://github.com/appium/appium/commit/66922279b7742a08613f472585a4a1cb70f80683))
|
||||
* **support:** update dependency axios to v1.4.0 ([91a6bc5](https://github.com/appium/appium/commit/91a6bc5925ab8ffc4ab6d05883900f7d186e49a9))
|
||||
* **types:** update dependency type-fest to v3.10.0 ([3c4d3ac](https://github.com/appium/appium/commit/3c4d3acc09d2ca1ed74dc77c18c62482e4c70239))
|
||||
* **types:** update dependency type-fest to v3.9.0 ([94a207f](https://github.com/appium/appium/commit/94a207fc9718068f3657c51cc8be0ef682f16b11))
|
||||
|
||||
|
||||
|
||||
## [9.3.7](https://github.com/appium/appium/compare/@appium/base-driver@9.3.6...@appium/base-driver@9.3.7) (2023-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **basedriver:** allow arbitrary session data to be returned by getSession ([6245022](https://github.com/appium/appium/commit/6245022be953b69acad70d0a4c877bc65eed2d3a))
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import './event';
|
||||
import './find';
|
||||
import './log';
|
||||
import './session';
|
||||
import './settings';
|
||||
import './timeout';
|
||||
import './execute';
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import {Constraints, ISessionCommands, MultiSessionData, SingularSessionData} from '@appium/types';
|
||||
import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
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<C>[] = [];
|
||||
|
||||
if (this.sessionId) {
|
||||
ret.push({
|
||||
id: this.sessionId,
|
||||
capabilities: this.caps,
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns capabilities for the session and event history (if applicable)
|
||||
*/
|
||||
async getSession<C extends Constraints>(this: BaseDriver<C>) {
|
||||
return <SingularSessionData<C>>(
|
||||
(this.caps.eventTimings ? {...this.caps, events: this.eventHistory} : this.caps)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
mixin(SessionCommands);
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
DriverCaps,
|
||||
DriverData,
|
||||
DriverOpts,
|
||||
MultiSessionData,
|
||||
ServerArgs,
|
||||
SingularSessionData,
|
||||
StringRecord,
|
||||
W3CDriverCaps,
|
||||
} from '@appium/types';
|
||||
@@ -31,7 +33,8 @@ const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
|
||||
export class BaseDriver<
|
||||
C extends Constraints,
|
||||
CArgs extends StringRecord = StringRecord,
|
||||
Settings extends StringRecord = StringRecord
|
||||
Settings extends StringRecord = StringRecord,
|
||||
SessionData extends StringRecord = StringRecord
|
||||
>
|
||||
extends DriverCore<C, Settings>
|
||||
implements Driver<C, CArgs>
|
||||
@@ -310,6 +313,25 @@ export class BaseDriver<
|
||||
|
||||
return [this.sessionId, caps];
|
||||
}
|
||||
async getSessions() {
|
||||
const ret: MultiSessionData<C>[] = [];
|
||||
|
||||
if (this.sessionId) {
|
||||
ret.push({
|
||||
id: this.sessionId,
|
||||
capabilities: this.caps,
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns capabilities for the session and event history (if applicable)
|
||||
*/
|
||||
async getSession() {
|
||||
return this.caps.eventTimings ? {...this.caps, events: this.eventHistory} : this.caps;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async deleteSession(sessionId?: string | null) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import B from 'bluebird';
|
||||
import {formatResponseValue, formatStatus} from './helpers';
|
||||
import {MAX_LOG_BODY_LENGTH, PROTOCOLS, DEFAULT_BASE_PATH} from '../constants';
|
||||
import {isW3cCaps} from '../helpers/capabilities';
|
||||
import log from '../basedriver/logger';
|
||||
|
||||
const CREATE_SESSION_COMMAND = 'createSession';
|
||||
const DELETE_SESSION_COMMAND = 'deleteSession';
|
||||
@@ -225,7 +226,7 @@ function validateExecuteMethodParams(params, paramSpec) {
|
||||
`arguments to execute script and instead received: ${JSON.stringify(params)}`
|
||||
);
|
||||
}
|
||||
const args = params[0] ?? {};
|
||||
let args = params[0] ?? {};
|
||||
if (!_.isPlainObject(args)) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Did not receive an appropriate execute method parameters object. It needs to be ` +
|
||||
@@ -237,6 +238,11 @@ function validateExecuteMethodParams(params, paramSpec) {
|
||||
} else {
|
||||
paramSpec.required ??= [];
|
||||
paramSpec.optional ??= [];
|
||||
const unknownNames = _.difference(_.keys(args), paramSpec.required, paramSpec.optional);
|
||||
if (!_.isEmpty(unknownNames)) {
|
||||
log.info(`The following script arguments are not known and will be ignored: ${unknownNames}`);
|
||||
args = _.pickBy(args, (v, k) => !unknownNames.includes(k));
|
||||
}
|
||||
checkParams(paramSpec, args, null);
|
||||
}
|
||||
const argsToApply = makeArgs({}, args, paramSpec, null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "9.3.5",
|
||||
"version": "9.3.12",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,17 +44,18 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/types": "^0.10.3",
|
||||
"@appium/support": "^4.0.3",
|
||||
"@appium/types": "^0.12.0",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/async-lock": "1.4.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/express": "4.17.17",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/method-override": "0.0.32",
|
||||
"@types/serve-favicon": "2.5.3",
|
||||
"@types/serve-favicon": "2.5.4",
|
||||
"async-lock": "1.4.0",
|
||||
"asyncbox": "2.9.4",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.4.0",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.2",
|
||||
"es6-error": "4.1.1",
|
||||
@@ -66,7 +67,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "3.7.2",
|
||||
"type-fest": "3.11.1",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.2.12](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.11...@appium/base-plugin@2.2.12) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.11](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.10...@appium/base-plugin@2.2.11) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.10](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.9...@appium/base-plugin@2.2.10) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.9](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.8...@appium/base-plugin@2.2.9) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.8](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.7...@appium/base-plugin@2.2.8) (2023-05-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.7](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.6...@appium/base-plugin@2.2.7) (2023-04-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Appium Base Plugin
|
||||
|
||||
The base class used to create Appium plugins. This plugin should not be installed directly as it does nothing. Instead, you extend this plugin when creating your *own* Appium plugins. Check out the [Building Plugins](https://appium.github.io/appium/docs/en/latest/ecosystem/build-plugins/) to learn how to make use of this base class to develop Appium plugins.
|
||||
The base class used to create Appium plugins. This plugin should not be installed directly as it does nothing. Instead, you extend this plugin when creating your *own* Appium plugins. Check out the [Building Plugins](https://appium.io/docs/en/latest/ecosystem/build-plugins/) to learn how to make use of this base class to develop Appium plugins.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.2.5",
|
||||
"version": "2.2.12",
|
||||
"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.5",
|
||||
"@appium/support": "^3.1.9"
|
||||
"@appium/base-driver": "^9.3.12",
|
||||
"@appium/support": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,68 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.18](https://github.com/appium/appium/compare/@appium/doctor@2.0.17...@appium/doctor@2.0.18) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.17](https://github.com/appium/appium/compare/@appium/doctor@2.0.16...@appium/doctor@2.0.17) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **doctor:** update dependency appium-adb to v9.11.3 ([a0b434d](https://github.com/appium/appium/commit/a0b434dff7a4e425f66915e12266a6e943d10372))
|
||||
* **doctor:** update dependency appium-adb to v9.11.4 ([f34c75c](https://github.com/appium/appium/commit/f34c75c65068a299ab4d3427233e771e796fc43a))
|
||||
* **doctor:** update dependency appium-adb to v9.11.5 ([78c6794](https://github.com/appium/appium/commit/78c67942534705133093aa9fa54d84efb9fe2f69))
|
||||
|
||||
|
||||
|
||||
## [2.0.16](https://github.com/appium/appium/compare/@appium/doctor@2.0.15...@appium/doctor@2.0.16) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.15](https://github.com/appium/appium/compare/@appium/doctor@2.0.14...@appium/doctor@2.0.15) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **doctor:** update dependency appium-adb to v9.11.2 ([e7df6fd](https://github.com/appium/appium/commit/e7df6fd4f054d6ad6eb4125f77e7e0d58bc05ebf))
|
||||
* **docutils:** update dependency yargs to v17.7.2 ([3d1e4af](https://github.com/appium/appium/commit/3d1e4af858ea21bcf3ab1e1015aa7bdf1776f3ce))
|
||||
|
||||
|
||||
|
||||
## [2.0.14](https://github.com/appium/appium/compare/@appium/doctor@2.0.13...@appium/doctor@2.0.14) (2023-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **doctor:** update dependency appium-adb to v9.11.1 ([45cd434](https://github.com/appium/appium/commit/45cd43488ce3e7a3c32864912d8fb104934f90d8))
|
||||
|
||||
|
||||
|
||||
## [2.0.13](https://github.com/appium/appium/compare/@appium/doctor@2.0.12...@appium/doctor@2.0.13) (2023-04-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/doctor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class OptionalFfmpegCommandCheck extends DoctorCheck {
|
||||
return ffmpegPath
|
||||
? okOptional(
|
||||
`ffmpeg is installed at: ${ffmpegPath}. ${
|
||||
(await exec('ffmpeg', ['-version'])).stdout.split(EOL)[0]
|
||||
(await exec('ffmpeg', ['-version'])).stdout.split('\n')[0]
|
||||
}`
|
||||
)
|
||||
: nokOptional('ffmpeg cannot be found');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/doctor",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.18",
|
||||
"description": "Test environment for fitness to run Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,17 +44,17 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/support": "^4.0.3",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"appium-adb": "9.11.0",
|
||||
"appium-adb": "9.11.5",
|
||||
"bluebird": "3.7.2",
|
||||
"inquirer": "8.2.5",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"yargs": "17.7.1"
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from 'node:path';
|
||||
import {
|
||||
EnvVarAndPathCheck,
|
||||
AndroidToolCheck,
|
||||
OptionalAppBundleCheck,
|
||||
OptionalGstreamerCheck,
|
||||
} from '../../lib/android';
|
||||
import {fs} from '@appium/support';
|
||||
import {fs, system} from '@appium/support';
|
||||
import * as adb from 'appium-adb';
|
||||
import * as utils from '../../lib/utils';
|
||||
import * as tp from 'teen_process';
|
||||
@@ -13,6 +14,9 @@ import B from 'bluebird';
|
||||
import {removeColors} from './helper';
|
||||
|
||||
describe('android', function () {
|
||||
const apkAnalyzerFilename = system.isWindows() ? 'apkanalyzer.bat' : 'apkanalyzer';
|
||||
const gstLaunchFilename = system.isWindows() ? 'gst-launch-1.0.exe' : 'gst-launch-1.0';
|
||||
const gstInspectFilename = system.isWindows() ? 'gst-inspect-1.0.exe' : 'gst-inspect-1.0';
|
||||
describe(
|
||||
'EnvVarAndPathCheck',
|
||||
withMocks({fs}, (mocks) => {
|
||||
@@ -64,12 +68,14 @@ describe('android', function () {
|
||||
});
|
||||
it('diagnose - success', async function () {
|
||||
process.env.ANDROID_HOME = '/a/b/c/d';
|
||||
mocks.adb.expects('getAndroidBinaryPath').exactly(check.tools.length)
|
||||
mocks.adb
|
||||
.expects('getAndroidBinaryPath')
|
||||
.exactly(check.tools.length)
|
||||
.returns(B.resolve('/path/to/binary'));
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: true,
|
||||
optional: false,
|
||||
message: 'adb, emulator, apkanalyzer exist: /a/b/c/d',
|
||||
message: `adb, emulator, ${apkAnalyzerFilename} exist: /a/b/c/d`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
@@ -79,19 +85,17 @@ describe('android', function () {
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: false,
|
||||
optional: false,
|
||||
message: 'adb, emulator, apkanalyzer could not be found because ' +
|
||||
'ANDROID_HOME or ANDROID_SDK_ROOT is NOT set!',
|
||||
message: `adb, emulator, ${apkAnalyzerFilename} could not be found because ANDROID_HOME or ANDROID_SDK_ROOT is NOT set!`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
it('diagnose - failure - path not valid', async function () {
|
||||
process.env.ANDROID_HOME = '/a/b/c/d';
|
||||
mocks.adb.expects('getAndroidBinaryPath').exactly(check.tools.length)
|
||||
.throws();
|
||||
mocks.adb.expects('getAndroidBinaryPath').exactly(check.tools.length).throws();
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: false,
|
||||
optional: false,
|
||||
message: 'adb, emulator, apkanalyzer could NOT be found in /a/b/c/d!',
|
||||
message: `adb, emulator, ${apkAnalyzerFilename} could NOT be found in /a/b/c/d!`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
@@ -102,11 +106,9 @@ describe('android', function () {
|
||||
);
|
||||
});
|
||||
it('fix - install', async function () {
|
||||
process.env.ANDROID_HOME = '/a/b/c/d';
|
||||
process.env.ANDROID_HOME = path.resolve('/', 'a', 'b', 'c', 'd');
|
||||
removeColors(await check.fix()).should.equal(
|
||||
'Manually install adb, emulator, apkanalyzer and add it to PATH. ' +
|
||||
'https://developer.android.com/studio#cmdline-tools and ' +
|
||||
'https://developer.android.com/studio/intro/update#sdk-manager may help to setup.'
|
||||
`Manually install adb, emulator, ${apkAnalyzerFilename} and add it to PATH. https://developer.android.com/studio#cmdline-tools and https://developer.android.com/studio/intro/update#sdk-manager may help to setup.`
|
||||
);
|
||||
});
|
||||
})
|
||||
@@ -138,8 +140,10 @@ describe('android', function () {
|
||||
mocks.verify();
|
||||
});
|
||||
it('fix', async function () {
|
||||
removeColors(await check.fix()).should.equal(
|
||||
'bundletool.jar is used to handle Android App Bundle. Please read http://appium.io/docs/en/writing-running-appium/android/android-appbundle/ to install it'
|
||||
removeColors(await check.fix()).should.match(
|
||||
new RegExp(
|
||||
'bundletool.jar is used to handle Android App Bundle. Please read http://appium.io/docs/en/writing-running-appium/android/android-appbundle/ to install it'
|
||||
)
|
||||
);
|
||||
});
|
||||
})
|
||||
@@ -158,8 +162,7 @@ describe('android', function () {
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: true,
|
||||
optional: true,
|
||||
message:
|
||||
'gst-launch-1.0 and gst-inspect-1.0 are installed at: path/to/gst-launch and path/to/gst-inspect',
|
||||
message: `${gstLaunchFilename} and ${gstInspectFilename} are installed at: path/to/gst-launch and path/to/gst-inspect`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
@@ -168,14 +171,13 @@ describe('android', function () {
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: false,
|
||||
optional: true,
|
||||
message: 'gst-launch-1.0 and/or gst-inspect-1.0 cannot be found',
|
||||
message: `${gstLaunchFilename} and/or ${gstInspectFilename} cannot be found`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
it('fix', async function () {
|
||||
removeColors(await check.fix()).should.equal(
|
||||
'gst-launch-1.0 and gst-inspect-1.0 are used to stream the screen of the device under test. ' +
|
||||
'Please read https://appium.io/docs/en/writing-running-appium/android/android-screen-streaming/ to install them and for more details'
|
||||
`${gstLaunchFilename} and ${gstInspectFilename} are used to stream the screen of the device under test. Please read https://appium.io/docs/en/writing-running-appium/android/android-screen-streaming/ to install them and for more details`
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import {BinaryIsInPathCheck, AndroidSdkExists} from '../../lib/dev';
|
||||
import {fs} from '@appium/support';
|
||||
import * as tp from 'teen_process';
|
||||
@@ -65,12 +66,12 @@ describe('dev', function () {
|
||||
check.autofix.should.not.be.ok;
|
||||
});
|
||||
it('diagnose - success', async function () {
|
||||
process.env.ANDROID_HOME = '/a/b/c/d';
|
||||
process.env.ANDROID_HOME = path.resolve('/', 'a', 'b', 'c', 'd');
|
||||
mocks.fs.expects('exists').once().returns(B.resolve(true));
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: true,
|
||||
optional: false,
|
||||
message: 'android-16 was found at: /a/b/c/d/platforms/android-16',
|
||||
message: `android-16 was found at: ${process.env.ANDROID_HOME}${path.sep}platforms${path.sep}android-16`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
@@ -84,12 +85,12 @@ describe('dev', function () {
|
||||
mocks.verify();
|
||||
});
|
||||
it('diagnose - failure - invalid path', async function () {
|
||||
process.env.ANDROID_HOME = '/a/b/c/d';
|
||||
process.env.ANDROID_HOME = path.resolve('/', 'a', 'b', 'c', 'd');
|
||||
mocks.fs.expects('exists').once().returns(B.resolve(false));
|
||||
(await check.diagnose()).should.deep.equal({
|
||||
ok: false,
|
||||
optional: false,
|
||||
message: "android-16 could NOT be found at '/a/b/c/d/platforms/android-16'!",
|
||||
message: `android-16 could NOT be found at '${process.env.ANDROID_HOME}${path.sep}platforms${path.sep}android-16'!`,
|
||||
});
|
||||
mocks.verify();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,124 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.4.1](https://github.com/appium/appium/compare/@appium/docutils@0.4.0...@appium/docutils@0.4.1) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** fixes some weirdness with default behavior and the "implies" keyword ([#18751](https://github.com/appium/appium/issues/18751)) ([e5254b5](https://github.com/appium/appium/commit/e5254b5b4564450252fd3eb55b773c0d7c615316))
|
||||
|
||||
|
||||
|
||||
## [0.4.0](https://github.com/appium/appium/compare/@appium/docutils@0.3.13...@appium/docutils@0.4.0) (2023-06-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Allow to provide ipv6 addresses and handle broadcast addresses in logs ([#18674](https://github.com/appium/appium/issues/18674)) ([8f63dab](https://github.com/appium/appium/commit/8f63dabb5dca882e522026f243e3445f5418874b))
|
||||
* **docutils:** init: scafffold package.json ([2766dcc](https://github.com/appium/appium/commit/2766dcca4cfdc74904cc21f4881d748ed8ffd198))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** update dependency yaml to v2.3.0 ([325d237](https://github.com/appium/appium/commit/325d2372078ee01e8fb58613ffd13417f2527151))
|
||||
* **docutils:** update dependency yaml to v2.3.1 ([1a2260b](https://github.com/appium/appium/commit/1a2260bb15d75e4f4c23551dc61529108a7b0d88))
|
||||
* **support:** update dependency semver to v7.5.1 ([5a55509](https://github.com/appium/appium/commit/5a555090c5322318f845415d743ee5c645fb94a0))
|
||||
* **types:** update dependency type-fest to v3.11.0 ([19277f6](https://github.com/appium/appium/commit/19277f6e14a56e52b4669d633e148ad4a3da2c7a))
|
||||
* **types:** update dependency type-fest to v3.11.1 ([56499eb](https://github.com/appium/appium/commit/56499eb997b551739bed628f057de7987674ea7f))
|
||||
|
||||
|
||||
|
||||
## [0.3.13](https://github.com/appium/appium/compare/@appium/docutils@0.3.12...@appium/docutils@0.3.13) (2023-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** fix broken executable ([f0ebe2e](https://github.com/appium/appium/commit/f0ebe2e02f3e4164f07fd10ca36f460ff7a61682))
|
||||
|
||||
|
||||
|
||||
## [0.3.12](https://github.com/appium/appium/compare/@appium/docutils@0.3.11...@appium/docutils@0.3.12) (2023-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** add @types/which as it is exported ([9538c34](https://github.com/appium/appium/commit/9538c34131cd2f8a3c45039efd65678bc0980497))
|
||||
* **docutils:** exec mike/mkdocs via python -m ([426830d](https://github.com/appium/appium/commit/426830d14a348f163535a7f935a65b028008738d))
|
||||
* **docutils:** provide CJS executable for proper linking ([db73971](https://github.com/appium/appium/commit/db73971018b560ca47527173094720eee3d20a55))
|
||||
* **docutils:** run mkdocs via python -m ([5d24ea1](https://github.com/appium/appium/commit/5d24ea1ad294fa28f7ecce125e5342b081a84c83))
|
||||
* **docutils:** try python3 if python not in PATH ([7b17754](https://github.com/appium/appium/commit/7b177547865a8343625cf089c97cebfb93fd6955))
|
||||
|
||||
|
||||
|
||||
## [0.3.11](https://github.com/appium/appium/compare/@appium/docutils@0.3.10...@appium/docutils@0.3.11) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.10](https://github.com/appium/appium/compare/@appium/docutils@0.3.9...@appium/docutils@0.3.10) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** remove -t arg for mkdocs which prevents theme customizations (fix [#18536](https://github.com/appium/appium/issues/18536)) ([#18617](https://github.com/appium/appium/issues/18617)) ([cdfe7c3](https://github.com/appium/appium/commit/cdfe7c3d82a61776a784236140dff94ff1f34540))
|
||||
* **docutils:** update dependency yaml to v2.2.2 ([ddadd69](https://github.com/appium/appium/commit/ddadd6977d76a8a5ce1559d2a4ff4a31b58fb42f))
|
||||
* **docutils:** update dependency yargs to v17.7.2 ([3d1e4af](https://github.com/appium/appium/commit/3d1e4af858ea21bcf3ab1e1015aa7bdf1776f3ce))
|
||||
* **support:** update dependency semver to v7.5.0 ([c568523](https://github.com/appium/appium/commit/c568523e017a8b52bdf5132a48bbfde791de704f))
|
||||
* **types:** update dependency type-fest to v3.10.0 ([3c4d3ac](https://github.com/appium/appium/commit/3c4d3acc09d2ca1ed74dc77c18c62482e4c70239))
|
||||
* **types:** update dependency type-fest to v3.9.0 ([94a207f](https://github.com/appium/appium/commit/94a207fc9718068f3657c51cc8be0ef682f16b11))
|
||||
|
||||
|
||||
|
||||
## [0.3.9](https://github.com/appium/appium/compare/@appium/docutils@0.3.8...@appium/docutils@0.3.9) (2023-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** do not bail when no site_dir found ([c85b89e](https://github.com/appium/appium/commit/c85b89ecbe28f2fd720d73fca7cd11f283eb9a0a))
|
||||
|
||||
|
||||
|
||||
## [0.3.8](https://github.com/appium/appium/compare/@appium/docutils@0.3.7...@appium/docutils@0.3.8) (2023-04-18)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.7](https://github.com/appium/appium/compare/@appium/docutils@0.3.6...@appium/docutils@0.3.7) (2023-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency semver to v7.4.0 ([6ae86bd](https://github.com/appium/appium/commit/6ae86bd5c7be879a4d8852951d73eb2f68128df3))
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
'use strict';
|
||||
require('source-map-support').install();
|
||||
|
||||
const {main} = require('../build/lib/cli');
|
||||
const {getLogger} = require('../build/lib/logger');
|
||||
|
||||
const log = getLogger('cli');
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks
|
||||
main().catch((err) => {
|
||||
log.error('Caught otherwise-unhandled rejection (this is probably a bug):', err);
|
||||
});
|
||||
@@ -13,41 +13,45 @@ import {
|
||||
DEFAULT_SERVE_HOST,
|
||||
DEFAULT_SERVE_PORT,
|
||||
NAME_BIN,
|
||||
NAME_MIKE,
|
||||
NAME_MKDOCS_YML,
|
||||
NAME_PYTHON,
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readPackageJson, whichMike} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {findMike, findMkDocsYml, findPython, readPackageJson} from '../fs';
|
||||
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`
|
||||
* @param mikePath Path to `mike` executable
|
||||
* @param args Extra args to `mike build`
|
||||
* @param opts Extra options for `teen_process.Subprocess.start`
|
||||
* @param mikePath Path to `mike` executable
|
||||
*/
|
||||
async function doServe(
|
||||
mikePath: string,
|
||||
args: string[] = [],
|
||||
opts: SpawnBackgroundProcessOpts = {},
|
||||
mikePath?: string
|
||||
opts: SpawnBackgroundProcessOpts = {}
|
||||
) {
|
||||
mikePath = mikePath ?? (await whichMike());
|
||||
const finalArgs = ['serve', ...args];
|
||||
return spawnBackgroundProcess(mikePath, finalArgs, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `mike build`
|
||||
* @param mikePath Path to `mike` executable
|
||||
* @param args Extra args to `mike build`
|
||||
* @param opts Extra options to `teen_process.exec`
|
||||
* @param mikePath Path to `mike` executable
|
||||
*/
|
||||
async function doDeploy(args: string[] = [], opts: TeenProcessExecOptions = {}, mikePath?: string) {
|
||||
mikePath = mikePath ?? (await whichMike());
|
||||
async function doDeploy(
|
||||
mikePath: string,
|
||||
args: string[] = [],
|
||||
opts: TeenProcessExecOptions = {}
|
||||
) {
|
||||
const finalArgs = ['deploy', ...args];
|
||||
log.debug('Launching %s with args: %O', mikePath, finalArgs);
|
||||
log.debug('Executing %s via: %s %O', NAME_MIKE, mikePath, finalArgs);
|
||||
return await exec(mikePath, finalArgs, opts);
|
||||
}
|
||||
|
||||
@@ -90,6 +94,15 @@ export async function deploy({
|
||||
execOpts,
|
||||
}: DeployOpts = {}) {
|
||||
const stop = stopwatch('deploy');
|
||||
|
||||
const pythonPath = await findPython();
|
||||
|
||||
if (!pythonPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_PYTHON}3/${NAME_PYTHON} executable in PATH; please install Python v3`
|
||||
);
|
||||
}
|
||||
|
||||
mkDocsYmlPath = mkDocsYmlPath ?? (await findMkDocsYml(cwd));
|
||||
if (!mkDocsYmlPath) {
|
||||
throw new DocutilsError(
|
||||
@@ -112,6 +125,11 @@ export async function deploy({
|
||||
port,
|
||||
host,
|
||||
};
|
||||
|
||||
const mikePath = await findMike();
|
||||
if (!mikePath) {
|
||||
throw new DocutilsError(`Could not find ${NAME_MIKE} executable; please run "${NAME_BIN} init"`);
|
||||
}
|
||||
if (serve) {
|
||||
const mikeArgs = [
|
||||
...argify(_.pickBy(mikeOpts, (value) => _.isNumber(value) || Boolean(value))),
|
||||
@@ -122,8 +140,9 @@ export async function deploy({
|
||||
}
|
||||
stop(); // discard
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mikeArgs, serveOpts);
|
||||
await doServe(mikePath, mikeArgs, serveOpts);
|
||||
} else {
|
||||
log.info('Deploying into branch %s', branch);
|
||||
const mikeArgs = [
|
||||
...argify(
|
||||
_.omitBy(
|
||||
@@ -136,9 +155,9 @@ export async function deploy({
|
||||
if (alias) {
|
||||
mikeArgs.push(alias);
|
||||
}
|
||||
await doDeploy(mikeArgs, execOpts);
|
||||
await doDeploy(mikePath, mikeArgs, execOpts);
|
||||
|
||||
log.success('Mike finished deployment into branch %s (%dms)', branch, stop());
|
||||
log.success('Finished deployment into branch %s (%dms)', branch, stop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -75,6 +75,7 @@ export function parseNav(nav: MkDocsYmlNav): ParsedNavData[] {
|
||||
const queue: QueueItem[] = [{entries, keypath: ''}];
|
||||
|
||||
while (queue.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const {entries, keypath} = queue.shift()!;
|
||||
for (const [key, item] of entries) {
|
||||
if (_.isString(item)) {
|
||||
@@ -99,8 +100,10 @@ export function parseNav(nav: MkDocsYmlNav): ParsedNavData[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all items within the list of parsed nav data which correpsond to the header. This is
|
||||
* imperfect, as it's possible for the header string to appear in multiple places in the nav, but let's just ignore that until we can't.
|
||||
* Finds all items within the list of parsed nav data which correpsond to the header.
|
||||
*
|
||||
* This is imperfect, as it's possible for the header string to appear in multiple places in the
|
||||
* nav, but let's just ignore that until we can't.
|
||||
* @param navData Some parsed nav data
|
||||
* @param header Header name
|
||||
*/
|
||||
@@ -109,6 +112,8 @@ function filterHeaderItems(navData: ParsedNavData[], header: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a root header keypath (like a prefix), a numeric offset, and some parsed nav data, compute
|
||||
* a keypath
|
||||
*
|
||||
* @param rootHeaderKeypath Root keypath as determined by {@linkcode getRootHeaderKeypath}
|
||||
* @param offset Numeric offset within the array of items for the header
|
||||
@@ -135,16 +140,16 @@ function navDataDidChange(
|
||||
newNavData: Array<Omit<ParsedNavData, 'name'>>,
|
||||
navData: ParsedNavData[]
|
||||
): boolean {
|
||||
const diff = _.xorWith(
|
||||
newNavData,
|
||||
navData,
|
||||
(a, b) => a.keypath === b.keypath && a.fileOrUrl === b.fileOrUrl
|
||||
);
|
||||
// the result should be the items from newNavData which either don't appear in
|
||||
// navdata, or the items which do appear in navData but have a different fileOrUrl
|
||||
const matchedKeypaths = _.intersectionBy(newNavData, navData, 'keypath');
|
||||
const diff = _.xorBy(newNavData, matchedKeypaths, 'fileOrUrl');
|
||||
return !_.isEmpty(diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the "root" keypath for a particular header
|
||||
*
|
||||
* @param headerItems Some subset of the nav data having keypaths corresponding to `header`
|
||||
* @param header Header string
|
||||
* @returns The keypath up to the header string, inclusive
|
||||
@@ -153,6 +158,7 @@ function getRootHeaderKeypath(headerItems: ParsedNavData[], header: string) {
|
||||
// these are the parts of the keypath of the first item, which will contain the header string. it
|
||||
// dosn't matter whether we pick the first one or any one; they will all contain the same root
|
||||
// keypath by definition.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const rootHeaderKeypathParts = _.toPath(_.first(headerItems)!.keypath);
|
||||
|
||||
// this is the keypath up to the header string, inclusive.
|
||||
@@ -165,7 +171,6 @@ function getRootHeaderKeypath(headerItems: ParsedNavData[], header: string) {
|
||||
* by `dir`; if `all` is true, we just use the default header, because we will be returning a whole
|
||||
* lot of headers).
|
||||
*
|
||||
*
|
||||
* @param dir Abs path to directory containing markdown files generated by TypeDoc
|
||||
* @param mkDocsDocsDir Configured `docs_dir` or via options
|
||||
* @param navData Nav data parsed by {@linkcode parseNav}
|
||||
@@ -200,7 +205,6 @@ async function getNavItemsForDir(
|
||||
const offset = navHeaderItems.findIndex((item) => item.fileOrUrl === fileOrUrl);
|
||||
const newOffset = offset >= 0 ? offset : navHeaderItems.length;
|
||||
const data = navHeaderItems[offset];
|
||||
log.warn(rootHeaderKeypath, newOffset, data);
|
||||
const keypath = getKeypathForHeaderItem(rootHeaderKeypath, newOffset, data);
|
||||
newNavHeaderItems.push({keypath, fileOrUrl});
|
||||
}
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -5,50 +5,43 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {spawn, SpawnOptions} from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import {exec, TeenProcessExecOptions} from 'teen_process';
|
||||
import {NAME_BIN, NAME_MKDOCS, NAME_MKDOCS_YML, NAME_THEME} from '../constants';
|
||||
import {DEFAULT_SITE_DIR, NAME_BIN, NAME_MKDOCS, NAME_MKDOCS_YML, NAME_PYTHON} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readMkDocsYml, whichMkDocs} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {findMkDocsYml, findPython, readMkDocsYml} from '../fs';
|
||||
import {getLogger} from '../logger';
|
||||
import {relative, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('mkdocs');
|
||||
const log = getLogger('mkdocs');
|
||||
|
||||
/**
|
||||
* Runs `mkdocs serve`
|
||||
* @param pythonPath Path to Python 3 executable
|
||||
* @param args Extra args to `mkdocs build`
|
||||
* @param opts Extra options for `teen_process.Subprocess.start`
|
||||
* @param mkDocsPath Path to `mkdocs` executable
|
||||
*/
|
||||
async function doServe(
|
||||
pythonPath: string,
|
||||
args: string[] = [],
|
||||
opts: SpawnBackgroundProcessOpts = {},
|
||||
mkDocsPath?: string
|
||||
opts: SpawnBackgroundProcessOpts = {}
|
||||
) {
|
||||
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
|
||||
const finalArgs = ['serve', ...args];
|
||||
|
||||
log.debug('Launching %s with args: %s', mkDocsPath, finalArgs);
|
||||
return spawnBackgroundProcess(mkDocsPath, finalArgs, opts);
|
||||
const finalArgs = ['-m', NAME_MKDOCS, 'serve', ...args];
|
||||
log.debug('Executing %s via: %s, %O', NAME_MKDOCS, pythonPath, finalArgs);
|
||||
return spawnBackgroundProcess(pythonPath, finalArgs, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `mkdocs build`
|
||||
* @param pythonPath Path to Python 3 executable
|
||||
* @param args Extra args to `mkdocs build`
|
||||
* @param opts Extra options to `teen_process.exec`
|
||||
* @param mkDocsPath Path to `mkdocs` executable
|
||||
*/
|
||||
async function doBuild(
|
||||
args: string[] = [],
|
||||
opts: TeenProcessExecOptions = {},
|
||||
mkDocsPath?: string
|
||||
) {
|
||||
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
|
||||
const finalArgs = ['build', ...args];
|
||||
log.debug('Launching %s with args: %O', mkDocsPath, finalArgs);
|
||||
return await exec(mkDocsPath, finalArgs, opts);
|
||||
async function doBuild(pythonPath: string, args: string[] = [], opts: TeenProcessExecOptions = {}) {
|
||||
const finalArgs = ['-m', NAME_MKDOCS, 'build', ...args];
|
||||
log.debug('Executing %s via: %s, %O', NAME_MKDOCS, pythonPath, finalArgs);
|
||||
return await exec(pythonPath, finalArgs, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,13 +51,20 @@ async function doBuild(
|
||||
export async function buildSite({
|
||||
mkdocsYml: mkDocsYmlPath,
|
||||
siteDir,
|
||||
theme = NAME_THEME,
|
||||
cwd = process.cwd(),
|
||||
serve = false,
|
||||
serveOpts,
|
||||
execOpts,
|
||||
}: BuildMkDocsOpts = {}) {
|
||||
const stop = stopwatch('build-mkdocs');
|
||||
|
||||
const pythonPath = await findPython();
|
||||
if (!pythonPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_PYTHON}3/${NAME_PYTHON} executable in PATH; please install Python v3`
|
||||
);
|
||||
}
|
||||
|
||||
mkDocsYmlPath = mkDocsYmlPath
|
||||
? path.resolve(process.cwd(), mkDocsYmlPath)
|
||||
: await findMkDocsYml(cwd);
|
||||
@@ -73,24 +73,34 @@ export async function buildSite({
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`
|
||||
);
|
||||
}
|
||||
const mkdocsArgs = ['-f', mkDocsYmlPath, '-t', theme];
|
||||
const mkdocsArgs = ['-f', mkDocsYmlPath];
|
||||
if (siteDir) {
|
||||
mkdocsArgs.push('-d', siteDir);
|
||||
}
|
||||
if (serve) {
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mkdocsArgs, serveOpts);
|
||||
await doServe(pythonPath, mkdocsArgs, serveOpts);
|
||||
} else {
|
||||
await doBuild(mkdocsArgs, execOpts);
|
||||
log.info('Building site...');
|
||||
await doBuild(pythonPath, mkdocsArgs, execOpts);
|
||||
let relSiteDir;
|
||||
if (siteDir) {
|
||||
relSiteDir = relative(cwd, siteDir);
|
||||
} else {
|
||||
({site_dir: siteDir} = await readMkDocsYml(mkDocsYmlPath));
|
||||
log.debug('Found site_dir %s', siteDir);
|
||||
relSiteDir = relative(path.dirname(mkDocsYmlPath), siteDir!);
|
||||
if (siteDir) {
|
||||
log.debug('Found site_dir %s', siteDir);
|
||||
relSiteDir = relative(path.dirname(mkDocsYmlPath), siteDir);
|
||||
} else {
|
||||
log.warn(
|
||||
'No site_dir specified in args or %s; using default site_dir: %s',
|
||||
NAME_MKDOCS_YML,
|
||||
DEFAULT_SITE_DIR
|
||||
);
|
||||
relSiteDir = relative(cwd, DEFAULT_SITE_DIR);
|
||||
}
|
||||
}
|
||||
log.success('MkDocs finished building into %s (%dms)', relSiteDir, stop());
|
||||
log.success('Finished building site into %s (%dms)', relSiteDir, stop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -79,7 +79,10 @@ export async function checkMissingPaths<T extends Record<string, Options>>(
|
||||
|
||||
if (missingPaths.length) {
|
||||
return missingPaths
|
||||
.map(({id, path}) => `Path specified using --${_.kebabCase(id)} (${path}) does not exist`)
|
||||
.map(
|
||||
({id, path}) =>
|
||||
`Default or specified path via --${_.kebabCase(id)} (${path}) does not exist`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -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:',
|
||||
@@ -124,7 +124,7 @@ const opts = {
|
||||
remote: {
|
||||
alias: 'r',
|
||||
describe: 'Remote to push to',
|
||||
implies: ['deploy', 'push'],
|
||||
implies: 'push',
|
||||
group: BuildCommandGroup.Deploy,
|
||||
type: 'string',
|
||||
requiresArg: true,
|
||||
@@ -133,7 +133,7 @@ const opts = {
|
||||
},
|
||||
prefix: {
|
||||
describe: 'Subdirectory within <branch> to commit to',
|
||||
implies: ['deploy', 'branch'],
|
||||
implies: 'branch',
|
||||
group: BuildCommandGroup.Deploy,
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
@@ -168,9 +168,10 @@ const opts = {
|
||||
},
|
||||
rebase: {
|
||||
describe: 'Rebase <branch> with remote before deploy',
|
||||
implies: ['deploy', 'branch', 'remote'],
|
||||
implies: 'deploy',
|
||||
group: BuildCommandGroup.Deploy,
|
||||
type: 'boolean',
|
||||
defaultDescription: 'true',
|
||||
},
|
||||
serve: {
|
||||
describe: 'Start development server',
|
||||
@@ -220,6 +221,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:',
|
||||
@@ -30,6 +30,7 @@ const opts = {
|
||||
nargs: 1,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
dir: {
|
||||
default: '.',
|
||||
@@ -59,6 +60,7 @@ const opts = {
|
||||
group: InitCommandGroup.MkDocs,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
mkdocs: {
|
||||
default: true,
|
||||
@@ -74,6 +76,7 @@ const opts = {
|
||||
normalize: true,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
'package-json': {
|
||||
defaultDescription: './package.json',
|
||||
@@ -98,6 +101,7 @@ const opts = {
|
||||
normalize: true,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'python',
|
||||
},
|
||||
'repo-name': {
|
||||
defaultDescription: '(derived from --repo-url)',
|
||||
@@ -106,6 +110,7 @@ const opts = {
|
||||
nargs: 1,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
'repo-url': {
|
||||
defaultDescription: '(from package.json)',
|
||||
@@ -114,6 +119,7 @@ const opts = {
|
||||
nargs: 1,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
'site-description': {
|
||||
defaultDescription: '(from package.json)',
|
||||
@@ -122,6 +128,7 @@ const opts = {
|
||||
nargs: 1,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
'site-name': {
|
||||
defaultDescription: '(extension package name)',
|
||||
@@ -130,6 +137,7 @@ const opts = {
|
||||
nargs: 1,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'mkdocs',
|
||||
},
|
||||
'tsconfig-json': {
|
||||
defaultDescription: './tsconfig.json',
|
||||
@@ -139,12 +147,14 @@ const opts = {
|
||||
normalize: true,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'typescript',
|
||||
},
|
||||
typedoc: {
|
||||
default: true,
|
||||
description: 'Create typedoc.json if needed',
|
||||
group: InitCommandGroup.Behavior,
|
||||
type: 'boolean',
|
||||
implies: 'entry-point',
|
||||
},
|
||||
'typedoc-json': {
|
||||
defaultDescription: './typedoc.json',
|
||||
@@ -154,6 +164,17 @@ const opts = {
|
||||
normalize: true,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'typedoc',
|
||||
},
|
||||
'entry-point': {
|
||||
alias: 'e',
|
||||
describe: 'Source entry point of extension',
|
||||
group: InitCommandGroup.Paths,
|
||||
nargs: 1,
|
||||
normalize: false,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
implies: 'typedoc',
|
||||
},
|
||||
typescript: {
|
||||
default: true,
|
||||
@@ -167,6 +188,7 @@ const opts = {
|
||||
group: InitCommandGroup.Behavior,
|
||||
type: 'boolean',
|
||||
conflicts: 'force',
|
||||
implies: 'python',
|
||||
},
|
||||
} as const satisfies Record<string, Options>;
|
||||
|
||||
|
||||
@@ -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:',
|
||||
@@ -50,7 +50,7 @@ const opts = {
|
||||
},
|
||||
'python-path': {
|
||||
defaultDescription: '(derived from shell)',
|
||||
description: 'Path to python 3 executable',
|
||||
description: 'Path to python3 executable',
|
||||
group: ValidateCommandGroup.Paths,
|
||||
nargs: 1,
|
||||
normalize: true,
|
||||
@@ -81,6 +81,15 @@ const opts = {
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
},
|
||||
'typedoc-path': {
|
||||
defaultDescription: '(derived from shell)',
|
||||
description: 'Path to typedoc executable',
|
||||
group: ValidateCommandGroup.Paths,
|
||||
nargs: 1,
|
||||
normalize: true,
|
||||
requiresArg: true,
|
||||
type: 'string',
|
||||
},
|
||||
typescript: {
|
||||
default: true,
|
||||
description: 'Validate TypeScript environment',
|
||||
|
||||
@@ -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,21 +6,22 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
|
||||
import {fs} from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import {sync as readPkg} from 'read-pkg';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import yargs from 'yargs/yargs';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap, NAME_BIN} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {build, init, validate} from './command';
|
||||
import {findConfig} from './config';
|
||||
import {fs} from '@appium/support';
|
||||
import {sync as readPkg} from 'read-pkg';
|
||||
|
||||
const pkg = readPkg({cwd: fs.findRoot(__dirname)});
|
||||
const log = getLogger('cli');
|
||||
const IMPLICATIONS_FAILED_REGEX = /implications\s+failed:\n\s*(.+)\s->\s(.+)$/i;
|
||||
|
||||
const log = logger.withTag('cli');
|
||||
export async function main(argv = hideBin(process.argv)) {
|
||||
const config = await findConfig(argv);
|
||||
|
||||
@@ -56,37 +57,48 @@ 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(
|
||||
/**
|
||||
* Custom failure handler so we can log nicely.
|
||||
*/
|
||||
(msg: string | null, error) => {
|
||||
/**
|
||||
* yargs' default output if an "implication" fails (e.g., arg _A_ requires arg _B_) leaves much to be desired.
|
||||
*
|
||||
* @remarks Unfortunately, we do not have access to the parsed arguments object, since it may have failed parsing.
|
||||
* @param msg Implication failure message
|
||||
* @returns Whether the message was an implication failure
|
||||
*/
|
||||
const handleImplicationFailure = (msg: string | null): boolean => {
|
||||
let match: RegExpMatchArray | null | undefined;
|
||||
if (!(match = msg?.match(IMPLICATIONS_FAILED_REGEX))) {
|
||||
return false;
|
||||
}
|
||||
const [, arg, missingArg] = match;
|
||||
log.error(
|
||||
`Argument "--${arg}" requires "--${missingArg}"; note that "--${arg}" may be enabled by default`
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
// if it is a DocutilsError, it has nothing to do with the CLI
|
||||
if (error instanceof DocutilsError) {
|
||||
log.error(error.message);
|
||||
} else {
|
||||
y.showHelp();
|
||||
console.log();
|
||||
log.error(msg ?? error.message);
|
||||
|
||||
if (!handleImplicationFailure(msg)) {
|
||||
log.error(`\n\n${msg ?? error.message}`);
|
||||
}
|
||||
}
|
||||
y.exit(1, error);
|
||||
}
|
||||
|
||||
@@ -153,3 +153,8 @@ export const LogLevelMap = {
|
||||
* Default site nav header text
|
||||
*/
|
||||
export const DEFAULT_NAV_HEADER = 'Reference';
|
||||
|
||||
/**
|
||||
* If the user does not specify a site directory _and_ the `mkdocs.yml` doesn't either, use this dir.
|
||||
*/
|
||||
export const DEFAULT_SITE_DIR = 'site';
|
||||
|
||||
+74
-23
@@ -3,30 +3,31 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import findUp from 'find-up';
|
||||
import YAML from 'yaml';
|
||||
import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg';
|
||||
import path from 'node:path';
|
||||
import {JsonValue} from 'type-fest';
|
||||
import {fs} from '@appium/support';
|
||||
import findUp from 'find-up';
|
||||
import * as JSON5 from 'json5';
|
||||
import _ from 'lodash';
|
||||
import path from 'node:path';
|
||||
import _pkgDir from 'pkg-dir';
|
||||
import logger from './logger';
|
||||
import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg';
|
||||
import {JsonValue} from 'type-fest';
|
||||
import {Application, TypeDocReader} from 'typedoc';
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
NAME_TYPEDOC_JSON,
|
||||
NAME_MKDOCS_YML,
|
||||
NAME_PACKAGE_JSON,
|
||||
NAME_MKDOCS,
|
||||
NAME_NPM,
|
||||
NAME_PYTHON,
|
||||
NAME_MIKE,
|
||||
NAME_MKDOCS_YML,
|
||||
NAME_NPM,
|
||||
NAME_PACKAGE_JSON,
|
||||
NAME_PYTHON,
|
||||
NAME_TYPEDOC,
|
||||
NAME_TYPEDOC_JSON,
|
||||
} from './constants';
|
||||
import {DocutilsError} from './error';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml} from './model';
|
||||
import { exec } from 'teen_process';
|
||||
|
||||
const log = logger.withTag('fs');
|
||||
const log = getLogger('fs');
|
||||
|
||||
/**
|
||||
* Finds path to closest `package.json`
|
||||
@@ -207,27 +208,77 @@ export function safeWriteFile(filepath: string, content: JsonValue, overwrite =
|
||||
/**
|
||||
* `which` with memoization
|
||||
*/
|
||||
export const cachedWhich = _.memoize(fs.which);
|
||||
|
||||
/**
|
||||
* Finds `mkdocs` executable
|
||||
*/
|
||||
export const whichMkDocs = _.partial(cachedWhich, NAME_MKDOCS);
|
||||
const cachedWhich = _.memoize(fs.which as typeof import('which'));
|
||||
|
||||
/**
|
||||
* Finds `npm` executable
|
||||
*/
|
||||
export const whichNpm = _.partial(cachedWhich, NAME_NPM);
|
||||
export const whichNpm = _.partial(cachedWhich, NAME_NPM, {nothrow: true});
|
||||
|
||||
/**
|
||||
* Finds `python` executable
|
||||
*/
|
||||
export const whichPython = _.partial(cachedWhich, NAME_PYTHON);
|
||||
const whichPython = _.partial(cachedWhich, NAME_PYTHON, {nothrow: true});
|
||||
|
||||
/**
|
||||
* Finds `mike` executable
|
||||
* Finds `python3` executable
|
||||
*/
|
||||
export const whichMike = _.partial(cachedWhich, NAME_MIKE);
|
||||
const whichPython3 = _.partial(cachedWhich, `${NAME_PYTHON}3`, {nothrow: true});
|
||||
|
||||
/**
|
||||
* Finds `typedoc` executable
|
||||
*/
|
||||
const whichTypeDoc = _.partial(cachedWhich, NAME_TYPEDOC, {nothrow: true});
|
||||
|
||||
/**
|
||||
* `mike` cannot be invoked via `python -m`, so we need to find the script.
|
||||
*/
|
||||
export const findMike = _.partial(async () => {
|
||||
// see if it's in PATH
|
||||
let mikePath = await cachedWhich(NAME_MIKE, {nothrow: true});
|
||||
if (mikePath) {
|
||||
return mikePath;
|
||||
}
|
||||
// if it isn't, it may be in a user dir
|
||||
const pythonPath = await findPython();
|
||||
if (!pythonPath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// the user dir can be found this way.
|
||||
// usually it's something like ~/.local
|
||||
const {stdout} = await exec(pythonPath, ['-m', 'site', '--user-base']);
|
||||
if (stdout) {
|
||||
mikePath = path.join(stdout.trim(), 'bin', 'mike');
|
||||
if (await fs.isExecutable(mikePath)) {
|
||||
return mikePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds the `typedoc` executable.
|
||||
*
|
||||
* Looks in the `node_modules/.bin` dir from the current working directory, and if this fails, in the `node_modules` dir for the file which `.bin/typedoc` should be a symlink of, and if _that_ fails, try the `PATH`.
|
||||
*/
|
||||
export const findTypeDoc = _.memoize(async (cwd = process.cwd()): Promise<string | undefined> => {
|
||||
// .cmd is for win32, of course. note that glob _always_ uses posix dir separators.
|
||||
const globResult =
|
||||
(await fs.glob('node_modules/.bin/typedoc?(.cmd)', {cwd, nodir: true})) ??
|
||||
(await fs.glob('node_modules/**/typedoc/bin/typedoc', {cwd, nodir: true, follow: true}));
|
||||
return _.first(globResult) ?? (await whichTypeDoc());
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds the `python3` or `python` executable in the user's `PATH`.
|
||||
*
|
||||
* `python3` is preferred over `python`, since the latter could be Python 2.
|
||||
*/
|
||||
export const findPython = _.memoize(
|
||||
async (): Promise<string | undefined> => (await whichPython3()) ?? (await whichPython())
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads an `mkdocs.yml` file, merges inherited configs, and returns the result. The result is cached.
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './builder';
|
||||
export * from './validate';
|
||||
export * from './scaffold';
|
||||
export * from './constants';
|
||||
export * from './logger';
|
||||
|
||||
@@ -11,16 +11,17 @@ import {
|
||||
NAME_PYTHON,
|
||||
REQUIREMENTS_TXT_PATH,
|
||||
NAME_TYPEDOC_JSON,
|
||||
NAME_PACKAGE_JSON,
|
||||
} from './constants';
|
||||
import YAML from 'yaml';
|
||||
import {exec} from 'teen_process';
|
||||
import {Simplify} from 'type-fest';
|
||||
import {PackageJson, 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';
|
||||
import {findPython, stringifyJson5, stringifyYaml} from './fs';
|
||||
|
||||
/**
|
||||
* Data for the base `mkdocs.yml` file
|
||||
@@ -54,9 +55,19 @@ const BASE_TSCONFIG_JSON: Readonly<TsConfigJson> = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const dryRunLog = log.withTag('dry-run');
|
||||
/**
|
||||
* Data for the base `package.json` file.
|
||||
* We expect `package.json` to exist, and we are not in the business of creating it.
|
||||
* However, we will need to add a `typedoc.entryPoint` prop to it.
|
||||
*/
|
||||
const BASE_PACKAGE_JSON: Readonly<PackageJson> = Object.freeze({});
|
||||
|
||||
const log = getLogger('init');
|
||||
const dryRunLog = getLogger('dry-run', log);
|
||||
|
||||
/**
|
||||
* Files included, by default, in `tsconfig.json`
|
||||
*/
|
||||
const DEFAULT_INCLUDE = ['lib', 'test', 'index.js'];
|
||||
/**
|
||||
* Function which scaffolds a `tsconfig.json` file
|
||||
@@ -97,6 +108,26 @@ export const initTypeDocJson = createScaffoldTask<InitTypeDocOptions, TypeDocJso
|
||||
'TypeDoc configuration'
|
||||
);
|
||||
|
||||
/**
|
||||
* Function which scaffolds a `package.json` file
|
||||
*
|
||||
* This only amends prop `typedoc.entryPoint` to the `package.json` file.
|
||||
*
|
||||
* If, strangely, `package.json` did not exist, then it will now contain _only_ that prop.
|
||||
*/
|
||||
export const initTypeDocPkgJson = createScaffoldTask<InitTypeDocOptions, PackageJson>(
|
||||
NAME_PACKAGE_JSON,
|
||||
BASE_PACKAGE_JSON,
|
||||
'Package configuration for TypeDoc',
|
||||
{
|
||||
transform: (content, opts) =>
|
||||
({
|
||||
...content,
|
||||
typedoc: {entryPoint: opts.typeDocEntryPoint},
|
||||
} as PackageJson),
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Function which scaffolds an `mkdocs.yml` file
|
||||
*/
|
||||
@@ -158,10 +189,12 @@ export const initMkDocs = createScaffoldTask<InitMkDocsOptions, MkDocsYml>(
|
||||
* @param opts Options
|
||||
*/
|
||||
export async function initPython({
|
||||
pythonPath = NAME_PYTHON,
|
||||
pythonPath,
|
||||
dryRun = false,
|
||||
upgrade = false,
|
||||
}: InitPythonOptions = {}): Promise<void> {
|
||||
pythonPath = pythonPath ?? (await findPython()) ?? NAME_PYTHON;
|
||||
|
||||
const args = ['-m', 'pip', 'install', '-r', REQUIREMENTS_TXT_PATH];
|
||||
if (upgrade) {
|
||||
args.push('--upgrade');
|
||||
@@ -222,6 +255,7 @@ export async function init({
|
||||
pythonPath,
|
||||
upgrade,
|
||||
typedocJson: typeDocJsonPath,
|
||||
entryPoint: typeDocEntryPoint,
|
||||
}: InitOptions = {}): Promise<void> {
|
||||
if (!typescript && typedoc) {
|
||||
log.warn(
|
||||
@@ -248,6 +282,13 @@ export async function init({
|
||||
dryRun,
|
||||
cwd,
|
||||
});
|
||||
await initTypeDocPkgJson({
|
||||
packageJson: packageJsonPath,
|
||||
overwrite: true, // <-- always overwrite
|
||||
dryRun,
|
||||
cwd,
|
||||
typeDocEntryPoint,
|
||||
});
|
||||
}
|
||||
|
||||
if (python) {
|
||||
@@ -269,7 +310,10 @@ export async function init({
|
||||
}
|
||||
}
|
||||
|
||||
export type InitTypeDocOptions = ScaffoldTaskOptions;
|
||||
export interface InitTypeDocOptions extends ScaffoldTaskOptions {
|
||||
typeDocEntryPoint?: string;
|
||||
}
|
||||
|
||||
export interface InitTsConfigOptions extends ScaffoldTaskOptions {
|
||||
/**
|
||||
* List of source files (globs supported); typically `src` or `lib`
|
||||
@@ -332,5 +376,10 @@ export type InitOptions = Simplify<
|
||||
* If `true`, upgrade only
|
||||
*/
|
||||
upgrade?: boolean;
|
||||
|
||||
/**
|
||||
* Path to entry point of extension (source; not "main" field)
|
||||
*/
|
||||
entryPoint?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -6,20 +6,19 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import figures from 'figures';
|
||||
import logSymbols from 'log-symbols';
|
||||
import chalk, {ForegroundColor, BackgroundColor} from 'chalk';
|
||||
import chalk, {type BackgroundColor, type ForegroundColor} from 'chalk';
|
||||
import consola, {
|
||||
logType as LogType,
|
||||
ConsolaReporterLogObject,
|
||||
type Consola,
|
||||
type ConsolaReporterLogObject,
|
||||
FancyReporter,
|
||||
FancyReporterOptions,
|
||||
Consola,
|
||||
ConsolaOptions,
|
||||
LogLevel,
|
||||
type FancyReporterOptions,
|
||||
type LogLevel,
|
||||
type logType as LogType,
|
||||
} from 'consola';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap} from './constants';
|
||||
import figures from 'figures';
|
||||
import _ from 'lodash';
|
||||
import logSymbols from 'log-symbols';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap} from './constants';
|
||||
|
||||
/**
|
||||
* This is a reporter for `consola` which uses some extra/custom icons and colors.
|
||||
@@ -137,57 +136,58 @@ class DocutilsReporter extends FancyReporter {
|
||||
let globalLevel: LogLevel = LogLevelMap[DEFAULT_LOG_LEVEL];
|
||||
|
||||
/**
|
||||
* The logger from which all loggers are created. This one uses a unique tag and our custom reporter.
|
||||
* Type guard to see if a string is a recognized log level
|
||||
* @param level any value
|
||||
*/
|
||||
const rootLogger = createLogProxy(
|
||||
consola.create({defaults: {tag: 'docutils'}, reporters: [new DocutilsReporter()]})
|
||||
);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
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);
|
||||
},
|
||||
});
|
||||
export function isLogLevelString(level: any): level is keyof typeof LogLevelMap {
|
||||
return level in LogLevelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The proxied root logger
|
||||
* @see {createLogProxy}
|
||||
* The logger from which all loggers are created. This one uses a unique tag and our custom reporter.
|
||||
*/
|
||||
export default rootLogger;
|
||||
const rootLogger = consola.create({
|
||||
defaults: {tag: 'docutils'},
|
||||
reporters: [new DocutilsReporter()],
|
||||
level: globalLevel,
|
||||
});
|
||||
// this prevents logging before `initLogger` is called
|
||||
rootLogger.pause();
|
||||
|
||||
/**
|
||||
* A map of tags to loggers
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
@@ -92,6 +92,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
const relativeDest = relativePath(dest);
|
||||
log.debug('Initializing %s', relativeDest);
|
||||
let shouldWriteDest = false;
|
||||
let isNew = false;
|
||||
let destContent: T;
|
||||
let result: ScaffoldTaskResult<T>;
|
||||
try {
|
||||
@@ -105,6 +106,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
shouldWriteDest = true;
|
||||
log.debug('Creating new file %s', relativeDest);
|
||||
destContent = {} as T;
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
const defaults: T = transform(defaultContent, opts, pkg);
|
||||
@@ -113,7 +115,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
shouldWriteDest = shouldWriteDest || !_.isEqual(destContent, finalDestContent);
|
||||
|
||||
if (shouldWriteDest) {
|
||||
log.info('Changes needed to %s', relativeDest);
|
||||
log.info('Changes needed in %s', relativeDest);
|
||||
log.debug('Original %s: %O', relativeDest, destContent);
|
||||
log.debug('Final %s: %O', relativeDest, finalDestContent);
|
||||
const patch = makePatch(dest, destContent, finalDestContent, serialize);
|
||||
@@ -126,6 +128,11 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
|
||||
try {
|
||||
await safeWriteFile(dest, finalDestContent, overwrite);
|
||||
if (isNew) {
|
||||
log.success('Initialized %s', description);
|
||||
} else {
|
||||
log.success('Updated %s', description);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
// this should only be thrown if `force` is false
|
||||
@@ -139,9 +146,9 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info('No changes to %s', relativeDest);
|
||||
log.info('No changes necessary for %s', relativeDest);
|
||||
}
|
||||
log.success('Initialized %s', description);
|
||||
log.success(`${description}: done`);
|
||||
return {path: dest, content: finalDestContent};
|
||||
};
|
||||
}
|
||||
@@ -153,7 +160,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
export type ScaffoldTaskTransformer<Opts extends ScaffoldTaskOptions, T extends JsonValue> = (
|
||||
content: Readonly<T>,
|
||||
opts: TaskSpecificOpts<Opts>,
|
||||
pkg: NormalizedPackageJson
|
||||
pkg: Readonly<NormalizedPackageJson>
|
||||
) => T;
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import _ from 'lodash';
|
||||
import {SpawnOptions, spawn} from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import type {SubProcess} from 'teen_process';
|
||||
|
||||
/**
|
||||
* Computes a relative path, prepending `./`
|
||||
@@ -26,7 +25,7 @@ export function stopwatch(id: string) {
|
||||
const start = Date.now();
|
||||
stopwatch.cache.set(id, start);
|
||||
return () => {
|
||||
const result = Date.now() - stopwatch.cache.get(id)!;
|
||||
const result = Date.now() - (stopwatch.cache.get(id) ?? 0);
|
||||
stopwatch.cache.delete(id);
|
||||
return result;
|
||||
};
|
||||
@@ -59,17 +58,16 @@ export const isStringArray = _.overEvery(_.isArray, _.partial(_.every, _, _.isSt
|
||||
export const argify: (obj: Record<string, string | number | boolean | undefined>) => string[] =
|
||||
_.flow(
|
||||
_.entries,
|
||||
_.flatten,
|
||||
(list) =>
|
||||
list.map((value, idx) => {
|
||||
list.map(([key, value]) => {
|
||||
if (value === true) {
|
||||
return `--${value}`;
|
||||
return [`--${key}`];
|
||||
} else if (value === false || value === undefined) {
|
||||
return;
|
||||
}
|
||||
return idx % 2 === 0 ? `--${value}` : value;
|
||||
return [`--${key}`, value];
|
||||
}),
|
||||
_.compact
|
||||
_.flatten
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,16 +31,16 @@ import {
|
||||
} from './constants';
|
||||
import {DocutilsError} from './error';
|
||||
import {
|
||||
findPkgDir,
|
||||
findMkDocsYml,
|
||||
findPkgDir,
|
||||
findTypeDoc,
|
||||
readJson5,
|
||||
readTypedocJson,
|
||||
whichMkDocs,
|
||||
whichNpm,
|
||||
whichPython,
|
||||
readMkDocsYml,
|
||||
readTypedocJson,
|
||||
whichNpm,
|
||||
findPython,
|
||||
} from './fs';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, PipPackage, TypeDocJson} from './model';
|
||||
import {relative} from './util';
|
||||
|
||||
@@ -62,9 +62,9 @@ const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
|
||||
/**
|
||||
* Matches the MkDocs version string from `mkdocs --version`
|
||||
*/
|
||||
const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
|
||||
const MKDOCS_VERSION_REGEX = /\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
|
||||
@@ -95,12 +95,12 @@ export class DocutilsValidator extends EventEmitter {
|
||||
/**
|
||||
* Path to `npm` executable.
|
||||
*/
|
||||
protected readonly npmPath: string | undefined;
|
||||
protected readonly npmPath?: string;
|
||||
|
||||
/**
|
||||
* Path to `python` executable.
|
||||
*/
|
||||
protected readonly pythonPath: string | undefined;
|
||||
protected readonly pythonPath?: string;
|
||||
|
||||
/**
|
||||
* List of validations to perform
|
||||
@@ -120,27 +120,29 @@ export class DocutilsValidator extends EventEmitter {
|
||||
/**
|
||||
* Path to `mkdocs.yml`. If not provided, will be lazily resolved.
|
||||
*/
|
||||
protected mkDocsYmlPath: string | undefined;
|
||||
protected mkDocsYmlPath?: string;
|
||||
|
||||
/**
|
||||
* Path to `package.json`. If not provided, will be lazily resolved.
|
||||
*/
|
||||
protected packageJsonPath: string | undefined;
|
||||
protected packageJsonPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the package directory. If not provided, will be lazily resolved.
|
||||
*/
|
||||
protected pkgDir: string | undefined;
|
||||
protected pkgDir?: string;
|
||||
|
||||
/**
|
||||
* Path to `tsconfig.json`. If not provided, will be lazily resolved.
|
||||
*/
|
||||
protected tsconfigJsonPath: string | undefined;
|
||||
protected tsconfigJsonPath?: string;
|
||||
|
||||
/**
|
||||
* Path to `typedoc.json`. If not provided, will be lazily resolved.
|
||||
*/
|
||||
protected typeDocJsonPath: string | undefined;
|
||||
protected typeDocJsonPath?: string;
|
||||
|
||||
protected typeDocPath?: string;
|
||||
|
||||
/**
|
||||
* Emitted when validation begins with a list of validation kinds to be performed
|
||||
@@ -181,6 +183,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
this.typeDocJsonPath = opts.typedocJson;
|
||||
this.npmPath = opts.npm;
|
||||
this.mkDocsYmlPath = opts.mkdocsYml;
|
||||
this.typeDocPath = opts.typedocPath;
|
||||
|
||||
if (opts.python) {
|
||||
this.validations.add(NAME_PYTHON);
|
||||
@@ -314,21 +317,19 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* Validates that the correct version of `mkdocs` is installed
|
||||
*/
|
||||
protected async validateMkDocs() {
|
||||
let mkDocsPath: string | undefined;
|
||||
try {
|
||||
mkDocsPath = await whichMkDocs();
|
||||
} catch {
|
||||
// _pretty sure_ the exception code is always ENOENT
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
|
||||
if (!pythonPath) {
|
||||
return this.fail(
|
||||
`Could not find ${NAME_MKDOCS} executable in PATH. If it is installed, check your PATH environment variable.`
|
||||
`Could not find ${NAME_PYTHON} executable in PATH. If it is installed, check your PATH environment variable.`
|
||||
);
|
||||
}
|
||||
|
||||
let rawMkDocsVersion: string | undefined;
|
||||
try {
|
||||
({stdout: rawMkDocsVersion} = await exec(mkDocsPath, ['--version']));
|
||||
({stdout: rawMkDocsVersion} = await exec(pythonPath, ['-m', NAME_MKDOCS, '--version']));
|
||||
} catch (err) {
|
||||
return this.fail(`${mkDocsPath} --version failed: ${err}`);
|
||||
return this.fail(`Failed to get MkDocs version: ${err}`);
|
||||
}
|
||||
const match = rawMkDocsVersion.match(MKDOCS_VERSION_REGEX);
|
||||
if (match) {
|
||||
@@ -343,12 +344,12 @@ export class DocutilsValidator extends EventEmitter {
|
||||
const {version: mkDocsReqdVersion} = mkDocsPipPkg;
|
||||
if (version !== mkDocsReqdVersion) {
|
||||
return this.fail(
|
||||
`${NAME_MKDOCS} at ${mkDocsPath} is v${version}, but ${REQUIREMENTS_TXT_PATH} requires v${mkDocsReqdVersion}`
|
||||
`${NAME_MKDOCS} is v${version}, but ${REQUIREMENTS_TXT_PATH} requires v${mkDocsReqdVersion}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new DocutilsError(
|
||||
`Could not parse version from "${mkDocsPath} --version". This is a bug. Output was ${rawMkDocsVersion}`
|
||||
`Could not parse version from MkDocs. This is a bug. Output was ${rawMkDocsVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -423,7 +424,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
*/
|
||||
protected async validatePythonDeps() {
|
||||
let pipListOutput: string;
|
||||
const pythonPath = this.pythonPath ?? (await whichPython());
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
if (!pythonPath) {
|
||||
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
|
||||
}
|
||||
@@ -499,7 +500,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* Asserts that the Python version is 3.x
|
||||
*/
|
||||
protected async validatePythonVersion() {
|
||||
const pythonPath = this.pythonPath ?? (await whichPython());
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
if (!pythonPath) {
|
||||
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
|
||||
}
|
||||
@@ -520,17 +521,30 @@ export class DocutilsValidator extends EventEmitter {
|
||||
/**
|
||||
* Asserts TypeDoc is installed, runnable, the correct version, and that the config file is readable
|
||||
* and constaints required options
|
||||
*
|
||||
* @todo Another option would be to `npm exec typedoc@<version>` which delegates to `npx`.
|
||||
*/
|
||||
protected async validateTypeDoc() {
|
||||
const pkgDir = await this.findPkgDir();
|
||||
const typeDocPath = this.typeDocPath ?? (await findTypeDoc(pkgDir));
|
||||
|
||||
if (!typeDocPath) {
|
||||
return this.fail(`Could not find ${NAME_TYPEDOC}; is it installed?`);
|
||||
}
|
||||
log.debug('Found %s at %s', NAME_TYPEDOC, typeDocPath);
|
||||
|
||||
let rawTypeDocVersion: string;
|
||||
let typeDocVersion: string;
|
||||
try {
|
||||
({stdout: rawTypeDocVersion} = await exec('npm', ['exec', NAME_TYPEDOC, '--', '--version'], {
|
||||
({stdout: rawTypeDocVersion} = await exec(process.execPath, [typeDocPath, '--version'], {
|
||||
cwd: pkgDir,
|
||||
}));
|
||||
} catch {
|
||||
return this.fail(`Could not find ${NAME_TYPEDOC} executable from ${pkgDir}`);
|
||||
} catch (err) {
|
||||
return this.fail(
|
||||
`Could not execute ${process.execPath} ${typeDocPath} from ${pkgDir}. Reason: ${
|
||||
(err as Error).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (rawTypeDocVersion) {
|
||||
@@ -543,7 +557,8 @@ export class DocutilsValidator extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
const reqdTypeDocVersion = DOCUTILS_PKG.dependencies!.typedoc!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const reqdTypeDocVersion = DOCUTILS_PKG.dependencies!.typedoc!; // this is our own package.json
|
||||
if (!satisfies(typeDocVersion, reqdTypeDocVersion)) {
|
||||
return this.fail(
|
||||
`Found TypeDoc version ${typeDocVersion}, but ${reqdTypeDocVersion} is required`
|
||||
@@ -596,7 +611,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
if (!typeDocJson.out) {
|
||||
return this.fail(
|
||||
new DocutilsError(
|
||||
`Missing "out" property in ${relTypeDocJsonPath}; path "${DEFAULT_REL_TYPEDOC_OUT_PATH} is recommended`
|
||||
`Missing "out" property in ${relTypeDocJsonPath}; path "${DEFAULT_REL_TYPEDOC_OUT_PATH}" is recommended`
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -714,6 +729,10 @@ export interface DocutilsValidatorOpts {
|
||||
* If `true`, run TypeDoc validation
|
||||
*/
|
||||
typedoc?: boolean;
|
||||
/**
|
||||
* Path to `typedoc` executable
|
||||
*/
|
||||
typedocPath?: string;
|
||||
/**
|
||||
* Path to `typedoc.json`
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "0.3.4",
|
||||
"version": "0.4.1",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -28,7 +28,7 @@
|
||||
"lib": "lib"
|
||||
},
|
||||
"bin": {
|
||||
"appium-docs": "./build/lib/cli/index.js"
|
||||
"appium-docs": "./bin/appium-docs.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
@@ -49,10 +49,11 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/tsconfig": "^0.2.4",
|
||||
"@appium/typedoc-plugin-appium": "^0.6.0",
|
||||
"@appium/support": "^4.0.3",
|
||||
"@appium/tsconfig": "^0.3.0",
|
||||
"@appium/typedoc-plugin-appium": "^0.6.5",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"@types/which": "3.0.0",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "2.15.3",
|
||||
"diff": "5.1.0",
|
||||
@@ -64,16 +65,16 @@
|
||||
"log-symbols": "4.1.0",
|
||||
"pkg-dir": "5.0.0",
|
||||
"read-pkg": "5.2.0",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.5.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.7.2",
|
||||
"type-fest": "3.11.1",
|
||||
"typedoc": "0.23.28",
|
||||
"typedoc-plugin-markdown": "3.14.0",
|
||||
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
|
||||
"typescript": "4.9.5",
|
||||
"yaml": "2.2.1",
|
||||
"yargs": "17.7.1",
|
||||
"yaml": "2.3.1",
|
||||
"yargs": "17.7.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.1.5
|
||||
mkdocs==1.4.3
|
||||
mkdocs-material==9.1.15
|
||||
mkdocs-material-extensions==1.1.1
|
||||
mike==1.1.2
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {argify} from '../../lib/util';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('argify', function () {
|
||||
it('should create args from params', function () {
|
||||
// deploy example
|
||||
const version = '2.0';
|
||||
const mikeOpts = {
|
||||
'config-file': '/path/to/yml',
|
||||
push: true,
|
||||
remote: 'origin',
|
||||
branch: 'gh-pages',
|
||||
prefix: '2.0',
|
||||
message: 'docs: a thing',
|
||||
rebase: true,
|
||||
port: 8100,
|
||||
host: 'localhost',
|
||||
};
|
||||
const mikeArgs = [
|
||||
...argify(
|
||||
_.omitBy(
|
||||
mikeOpts,
|
||||
(value, key) => _.includes(['port', 'host'], key) || (!_.isNumber(value) && !value)
|
||||
)
|
||||
),
|
||||
version,
|
||||
];
|
||||
mikeArgs.should.eql([
|
||||
'--config-file',
|
||||
'/path/to/yml',
|
||||
'--push',
|
||||
'--remote',
|
||||
'origin',
|
||||
'--branch',
|
||||
'gh-pages',
|
||||
'--prefix',
|
||||
'2.0',
|
||||
'--message',
|
||||
'docs: a thing',
|
||||
'--rebase',
|
||||
'2.0',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.4.7](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.6...@appium/driver-test-support@0.4.7) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **schema:** update definitelytyped ([e967240](https://github.com/appium/appium/commit/e96724077ef2b5b8aae203856195f3bb8de56116))
|
||||
* **test-support:** update dependency sinon to v15.1.0 ([4c61d53](https://github.com/appium/appium/commit/4c61d533b8f52131a637f5cd0b019375f85f075f))
|
||||
* **types:** update dependency type-fest to v3.11.0 ([19277f6](https://github.com/appium/appium/commit/19277f6e14a56e52b4669d633e148ad4a3da2c7a))
|
||||
* **types:** update dependency type-fest to v3.11.1 ([56499eb](https://github.com/appium/appium/commit/56499eb997b551739bed628f057de7987674ea7f))
|
||||
|
||||
|
||||
|
||||
## [0.4.6](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.5...@appium/driver-test-support@0.4.6) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency axios to v1.3.6 ([6692227](https://github.com/appium/appium/commit/66922279b7742a08613f472585a4a1cb70f80683))
|
||||
* **support:** update dependency axios to v1.4.0 ([91a6bc5](https://github.com/appium/appium/commit/91a6bc5925ab8ffc4ab6d05883900f7d186e49a9))
|
||||
* **test-support:** update dependency sinon to v15.0.4 ([9ad7279](https://github.com/appium/appium/commit/9ad7279fa70d7198a8d02f0f206397cb4a830e1d))
|
||||
* **types:** update dependency type-fest to v3.10.0 ([3c4d3ac](https://github.com/appium/appium/commit/3c4d3acc09d2ca1ed74dc77c18c62482e4c70239))
|
||||
* **types:** update dependency type-fest to v3.9.0 ([94a207f](https://github.com/appium/appium/commit/94a207fc9718068f3657c51cc8be0ef682f16b11))
|
||||
|
||||
|
||||
|
||||
## [0.4.5](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.4...@appium/driver-test-support@0.4.5) (2023-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **driver-test-support:** update dependency @types/lodash to v4.14.194 ([78bc092](https://github.com/appium/appium/commit/78bc0926db83f586db4683b008737b6254ec6ebe))
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import B from 'bluebird';
|
||||
import {TEST_HOST, getTestPort, createAppiumURL} from './helpers';
|
||||
import chai from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import { Agent } from 'node:http';
|
||||
|
||||
const should = chai.should();
|
||||
|
||||
@@ -148,6 +149,9 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
|
||||
describe('session handling', function () {
|
||||
it('should handle idempotency while creating sessions', async function () {
|
||||
// workaround for https://github.com/node-fetch/node-fetch/issues/1735
|
||||
const httpAgent = new Agent({ keepAlive: true });
|
||||
|
||||
const sessionIds = [];
|
||||
let times = 0;
|
||||
do {
|
||||
@@ -159,11 +163,7 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
headers: {
|
||||
'X-Idempotency-Key': '123456',
|
||||
},
|
||||
// XXX: I'm not sure what these are, as they are not documented axios options,
|
||||
// nor are they mentioned in our source
|
||||
// @ts-expect-error
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
httpAgent,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -178,6 +178,9 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
});
|
||||
|
||||
it('should handle idempotency while creating parallel sessions', async function () {
|
||||
// workaround for https://github.com/node-fetch/node-fetch/issues/1735
|
||||
const httpAgent = new Agent({ keepAlive: true });
|
||||
|
||||
const reqs = [];
|
||||
let times = 0;
|
||||
do {
|
||||
@@ -192,6 +195,7 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
|
||||
headers: {
|
||||
'X-Idempotency-Key': '12345',
|
||||
},
|
||||
httpAgent,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.7",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,18 +41,18 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.10.3",
|
||||
"@types/lodash": "4.14.192",
|
||||
"@appium/types": "^0.12.0",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/stoppable": "1.1.1",
|
||||
"axios": "1.3.4",
|
||||
"axios": "1.4.0",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "4.3.7",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "15.0.3",
|
||||
"sinon": "15.1.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"stoppable": "1.1.0",
|
||||
"type-fest": "3.7.2"
|
||||
"type-fest": "3.11.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [3.0.14](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.13...@appium/execute-driver-plugin@3.0.14) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **execute-driver-plugin:** update dependency vm2 to v3.9.19 ([8594d13](https://github.com/appium/appium/commit/8594d13bf6a1cb59bc76cbe2d39976ae86cdb3b2))
|
||||
|
||||
|
||||
|
||||
## [3.0.13](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.12...@appium/execute-driver-plugin@3.0.13) (2023-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **execute-driver-plugin:** update dependency vm2 to v3.9.17 ([61ca898](https://github.com/appium/appium/commit/61ca898c4730a821ea4b3b1e9c6a70a03d4787da))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v7.31.1 ([6499eea](https://github.com/appium/appium/commit/6499eea0af6aea1bfb69b1e36c4c445e3ab05b82))
|
||||
|
||||
|
||||
|
||||
## [3.0.12](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.11...@appium/execute-driver-plugin@3.0.12) (2023-04-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **execute-driver-plugin:** update dependency vm2 to v3.9.16 ([d6c70f2](https://github.com/appium/appium/commit/d6c70f2f526af4d91e6c2de940bbfa3208a7ffa4))
|
||||
|
||||
|
||||
|
||||
## [3.0.11](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.10...@appium/execute-driver-plugin@3.0.11) (2023-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **execute-driver-plugin:** update dependency vm2 to v3.9.15 ([5790924](https://github.com/appium/appium/commit/5790924bf89d55352bd5e20b14377b2bffc0cbf1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.10](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.9...@appium/execute-driver-plugin@3.0.10) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/execute-driver-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/execute-driver-plugin",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.14",
|
||||
"description": "Plugin for batching and executing driver commands with Appiums",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -42,8 +42,8 @@
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"vm2": "3.9.14",
|
||||
"webdriverio": "7.30.2"
|
||||
"vm2": "3.9.19",
|
||||
"webdriverio": "7.31.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35"
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.2.4](https://github.com/appium/appium/compare/@appium/fake-driver@5.2.3...@appium/fake-driver@5.2.4) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **universal-xml-plugin:** update dependency @xmldom/xmldom to v0.8.8 ([3bf4060](https://github.com/appium/appium/commit/3bf406015eb9854abbf6cf024e3bd724468c924d))
|
||||
|
||||
|
||||
|
||||
## [5.2.3](https://github.com/appium/appium/compare/@appium/fake-driver@5.2.2...@appium/fake-driver@5.2.3) (2023-04-03)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-driver",
|
||||
"version": "5.2.3",
|
||||
"version": "5.2.4",
|
||||
"description": "Mock driver used internally by Appium for testing. Ignore",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -43,11 +43,11 @@
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:smoke": "node ./build/lib/index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
"test:unit": "mocha --timeout 10s --slow 5s \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"@xmldom/xmldom": "0.8.8",
|
||||
"asyncbox": "2.9.4",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.16](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.15...@appium/fake-plugin@3.1.16) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.15](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.14...@appium/fake-plugin@3.1.15) (2023-06-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.14](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.13...@appium/fake-plugin@3.1.14) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.13](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.12...@appium/fake-plugin@3.1.13) (2023-05-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.12](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.11...@appium/fake-plugin@3.1.12) (2023-05-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.11](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.10...@appium/fake-plugin@3.1.11) (2023-04-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.10](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.9...@appium/fake-plugin@3.1.10) (2023-04-10)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.9](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.8...@appium/fake-plugin@3.1.9) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
@@ -59,7 +59,7 @@ class FakePlugin extends BasePlugin {
|
||||
FakePlugin._unexpectedData = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars,require-await
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,require-await
|
||||
static async updateServer(expressApp, httpServer, cliArgs) {
|
||||
expressApp.all('/fake', FakePlugin.fakeRoute);
|
||||
expressApp.all('/unexpected', FakePlugin.unexpectedData);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "3.1.9",
|
||||
"version": "3.1.16",
|
||||
"description": "A fake Appium 2.0 plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -39,8 +39,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^2.2.5",
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/base-plugin": "^2.2.12",
|
||||
"@appium/support": "^4.0.3",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.1.2](https://github.com/appium/appium/compare/@appium/images-plugin@2.1.1...@appium/images-plugin@2.1.2) (2023-06-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **images-plugin:** Make sure the template is always a buffer ([#18696](https://github.com/appium/appium/issues/18696)) ([0497090](https://github.com/appium/appium/commit/0497090a90e6219a383125543780cd71269a4604))
|
||||
|
||||
|
||||
|
||||
## [2.1.1](https://github.com/appium/appium/compare/@appium/images-plugin@2.1.0...@appium/images-plugin@2.1.1) (2023-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **images-plugin:** Add the missing dependency to support ([#18652](https://github.com/appium/appium/issues/18652)) ([f512e59](https://github.com/appium/appium/commit/f512e596eb54edb5ca8993b3451aa4a22184bc57))
|
||||
|
||||
|
||||
|
||||
## [2.1.0](https://github.com/appium/appium/compare/@appium/images-plugin@2.0.10...@appium/images-plugin@2.1.0) (2023-05-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **image-plugin:** Make it possible to find elements inside of other elements ([#18555](https://github.com/appium/appium/issues/18555)) ([2796639](https://github.com/appium/appium/commit/2796639b1183f0efbae8d7adb6d24570d5d4fde4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **opencv:** update dependency sharp to v0.32.1 ([add0eaa](https://github.com/appium/appium/commit/add0eaa05f476a5f2708925a43f2bc16cad75872))
|
||||
|
||||
|
||||
|
||||
## [2.0.10](https://github.com/appium/appium/compare/@appium/images-plugin@2.0.9...@appium/images-plugin@2.0.10) (2023-04-05)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import {errors} from 'appium/driver';
|
||||
import {getImagesMatches, getImagesSimilarity, getImageOccurrence} from '@appium/opencv';
|
||||
|
||||
const MATCH_FEATURES_MODE = 'matchFeatures';
|
||||
const GET_SIMILARITY_MODE = 'getSimilarity';
|
||||
const MATCH_TEMPLATE_MODE = 'matchTemplate';
|
||||
|
||||
const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE, MATCH_TEMPLATE_MODE} from './constants';
|
||||
|
||||
/**
|
||||
* Performs images comparison using OpenCV framework features.
|
||||
@@ -15,9 +10,9 @@ const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
*
|
||||
* @param {string} mode - One of possible comparison modes:
|
||||
* matchFeatures, getSimilarity, matchTemplate
|
||||
* @param {string} firstImage - Base64-encoded image file.
|
||||
* @param {string|Buffer} firstImage - Base64-encoded image file.
|
||||
* All image formats, that OpenCV library itself accepts, are supported.
|
||||
* @param {string} secondImage - Base64-encoded image file.
|
||||
* @param {string|Buffer} secondImage - Base64-encoded image file.
|
||||
* All image formats, that OpenCV library itself accepts, are supported.
|
||||
* @param {object} [options] - The content of this dictionary depends
|
||||
* on the actual `mode` value. See the documentation on `@appium/support`
|
||||
@@ -34,8 +29,8 @@ const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
* @type {import('@appium/types').PluginCommand<[string, string, string, object|undefined], Promise<OccurrenceResult>>}
|
||||
*/
|
||||
async function compareImages(mode, firstImage, secondImage, options = {}) {
|
||||
const img1 = Buffer.from(firstImage, 'base64');
|
||||
const img2 = Buffer.from(secondImage, 'base64');
|
||||
const img1 = Buffer.isBuffer(firstImage) ? firstImage : Buffer.from(firstImage, 'base64');
|
||||
const img2 = Buffer.isBuffer(secondImage) ? secondImage : Buffer.from(secondImage, 'base64');
|
||||
let result = null;
|
||||
switch (_.toLower(mode)) {
|
||||
case MATCH_FEATURES_MODE.toLowerCase():
|
||||
@@ -84,13 +79,7 @@ function convertVisualizationToBase64(element) {
|
||||
return element;
|
||||
}
|
||||
|
||||
export {
|
||||
compareImages,
|
||||
DEFAULT_MATCH_THRESHOLD,
|
||||
MATCH_TEMPLATE_MODE,
|
||||
MATCH_FEATURES_MODE,
|
||||
GET_SIMILARITY_MODE,
|
||||
};
|
||||
export {compareImages};
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/opencv').OccurrenceResult} OccurrenceResult
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {node} from 'appium/support';
|
||||
|
||||
export const IMAGE_STRATEGY = '-image';
|
||||
|
||||
export const IMAGE_ELEMENT_PREFIX = 'appium-image-element-';
|
||||
export const IMAGE_EL_TAP_STRATEGY_W3C = 'w3cActions';
|
||||
export const IMAGE_EL_TAP_STRATEGY_MJSONWP = 'touchActions';
|
||||
export const IMAGE_TAP_STRATEGIES = [IMAGE_EL_TAP_STRATEGY_MJSONWP, IMAGE_EL_TAP_STRATEGY_W3C];
|
||||
export const DEFAULT_TEMPLATE_IMAGE_SCALE = 1.0;
|
||||
|
||||
export const MATCH_FEATURES_MODE = 'matchFeatures';
|
||||
export const GET_SIMILARITY_MODE = 'getSimilarity';
|
||||
export const MATCH_TEMPLATE_MODE = 'matchTemplate';
|
||||
|
||||
export const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
|
||||
export const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
|
||||
|
||||
export const DEFAULT_SETTINGS = node.deepFreeze({
|
||||
// value between 0 and 1 representing match strength, below which an image
|
||||
// element will not be found
|
||||
imageMatchThreshold: DEFAULT_MATCH_THRESHOLD,
|
||||
|
||||
// One of possible image matching methods.
|
||||
// Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
|
||||
// for more details.
|
||||
// TM_CCOEFF_NORMED by default
|
||||
imageMatchMethod: '',
|
||||
|
||||
// if the image returned by getScreenshot differs in size or aspect ratio
|
||||
// from the screen, attempt to fix it automatically
|
||||
fixImageFindScreenshotDims: true,
|
||||
|
||||
// whether Appium should ensure that an image template sent in during image
|
||||
// element find should have its size adjusted so the match algorithm will not
|
||||
// complain
|
||||
fixImageTemplateSize: false,
|
||||
|
||||
// whether Appium should ensure that an image template sent in during image
|
||||
// element find should have its scale adjusted to display size so the match
|
||||
// algorithm will not complain.
|
||||
// e.g. iOS has `width=375, height=667` window rect, but its screenshot is
|
||||
// `width=750 × height=1334` pixels. This setting help to adjust the scale
|
||||
// if a user use `width=750 × height=1334` pixels's base template image.
|
||||
fixImageTemplateScale: false,
|
||||
|
||||
// Users might have scaled template image to reduce their storage size.
|
||||
// This setting allows users to scale a template image they send to Appium server
|
||||
// so that the Appium server compares the actual scale users originally had.
|
||||
// e.g. If a user has an image of 270 x 32 pixels which was originally 1080 x 126 pixels,
|
||||
// the user can set {defaultImageTemplateScale: 4.0} to scale the small image
|
||||
// to the original one so that Appium can compare it as the original one.
|
||||
defaultImageTemplateScale: DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
|
||||
// whether Appium should re-check that an image element can be matched
|
||||
// against the current screenshot before clicking it
|
||||
checkForImageElementStaleness: true,
|
||||
|
||||
// whether before clicking on an image element Appium should re-determine the
|
||||
// position of the element on screen
|
||||
autoUpdateImageElementPosition: false,
|
||||
|
||||
// which method to use for tapping by coordinate for image elements. the
|
||||
// options are 'w3c' or 'mjsonwp'
|
||||
imageElementTapStrategy: IMAGE_EL_TAP_STRATEGY_W3C,
|
||||
|
||||
// which method to use to save the matched image area in ImageElement class.
|
||||
// It is used for debugging purpose.
|
||||
getMatchedImageResult: false,
|
||||
});
|
||||
@@ -1,113 +1,72 @@
|
||||
import _ from 'lodash';
|
||||
import LRU from 'lru-cache';
|
||||
import {errors} from 'appium/driver';
|
||||
import {util, imageUtil} from 'appium/support';
|
||||
import {
|
||||
ImageElement,
|
||||
DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
IMAGE_EL_TAP_STRATEGY_W3C,
|
||||
} from './image-element';
|
||||
import {MATCH_TEMPLATE_MODE, compareImages, DEFAULT_MATCH_THRESHOLD} from './compare';
|
||||
import {ImageElement} from './image-element';
|
||||
import {compareImages} from './compare';
|
||||
import log from './logger';
|
||||
import {
|
||||
DEFAULT_SETTINGS, MATCH_TEMPLATE_MODE, DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
||||
} from './constants';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const MJSONWP_ELEMENT_KEY = 'ELEMENT';
|
||||
const W3C_ELEMENT_KEY = util.W3C_WEB_ELEMENT_IDENTIFIER;
|
||||
const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
|
||||
// Used to compare ratio and screen width
|
||||
// Pixel is basically under 1080 for example. 100K is probably enough fo a while.
|
||||
const FLOAT_PRECISION = 100000;
|
||||
const MAX_CACHE_ITEMS = 100;
|
||||
const MAX_CACHE_SIZE_BYTES = 1024 * 1024 * 40; // 40mb
|
||||
const MAX_CACHE_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
// value between 0 and 1 representing match strength, below which an image
|
||||
// element will not be found
|
||||
imageMatchThreshold: DEFAULT_MATCH_THRESHOLD,
|
||||
/**
|
||||
* Checks if one rect fully contains another
|
||||
*
|
||||
* @param {import('@appium/types').Rect} templateRect The bounding rect
|
||||
* @param {import('@appium/types').Rect} rect The rect to be checked for containment
|
||||
* @returns {boolean} True if templateRect contains rect
|
||||
*/
|
||||
function containsRect(templateRect, rect) {
|
||||
return templateRect.x <= rect.x && templateRect.y <= rect.y
|
||||
&& rect.width <= templateRect.x + templateRect.width - rect.x
|
||||
&& rect.height <= templateRect.y + templateRect.height - rect.y;
|
||||
}
|
||||
|
||||
// One of possible image matching methods.
|
||||
// Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
|
||||
// for more details.
|
||||
// TM_CCOEFF_NORMED by default
|
||||
imageMatchMethod: '',
|
||||
|
||||
// if the image returned by getScreenshot differs in size or aspect ratio
|
||||
// from the screen, attempt to fix it automatically
|
||||
fixImageFindScreenshotDims: true,
|
||||
|
||||
// whether Appium should ensure that an image template sent in during image
|
||||
// element find should have its size adjusted so the match algorithm will not
|
||||
// complain
|
||||
fixImageTemplateSize: false,
|
||||
|
||||
// whether Appium should ensure that an image template sent in during image
|
||||
// element find should have its scale adjusted to display size so the match
|
||||
// algorithm will not complain.
|
||||
// e.g. iOS has `width=375, height=667` window rect, but its screenshot is
|
||||
// `width=750 × height=1334` pixels. This setting help to adjust the scale
|
||||
// if a user use `width=750 × height=1334` pixels's base template image.
|
||||
fixImageTemplateScale: false,
|
||||
|
||||
// Users might have scaled template image to reduce their storage size.
|
||||
// This setting allows users to scale a template image they send to Appium server
|
||||
// so that the Appium server compares the actual scale users originally had.
|
||||
// e.g. If a user has an image of 270 x 32 pixels which was originally 1080 x 126 pixels,
|
||||
// the user can set {defaultImageTemplateScale: 4.0} to scale the small image
|
||||
// to the original one so that Appium can compare it as the original one.
|
||||
defaultImageTemplateScale: DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
|
||||
// whether Appium should re-check that an image element can be matched
|
||||
// against the current screenshot before clicking it
|
||||
checkForImageElementStaleness: true,
|
||||
|
||||
// whether before clicking on an image element Appium should re-determine the
|
||||
// position of the element on screen
|
||||
autoUpdateImageElementPosition: false,
|
||||
|
||||
// which method to use for tapping by coordinate for image elements. the
|
||||
// options are 'w3c' or 'mjsonwp'
|
||||
imageElementTapStrategy: IMAGE_EL_TAP_STRATEGY_W3C,
|
||||
|
||||
// which method to use to save the matched image area in ImageElement class.
|
||||
// It is used for debugging purpose.
|
||||
getMatchedImageResult: false,
|
||||
};
|
||||
const NO_OCCURRENCES_PATTERN = /Cannot find any occurrences/;
|
||||
const CONDITION_UNMET_PATTERN = /Condition unmet/;
|
||||
|
||||
|
||||
export default class ImageElementFinder {
|
||||
/** @type {ExternalDriver} */
|
||||
driver;
|
||||
|
||||
/** @type {LRU<string,ImageElement>} */
|
||||
imgElCache;
|
||||
_imgElCache;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ExternalDriver} driver
|
||||
* @param {number} [maxSize]
|
||||
* @param {number} max
|
||||
*/
|
||||
constructor(driver, maxSize = MAX_CACHE_SIZE_BYTES) {
|
||||
this.driver = driver;
|
||||
this.imgElCache = new LRU({
|
||||
max: MAX_CACHE_ITEMS,
|
||||
maxSize,
|
||||
sizeCalculation: (el) => el.template.length,
|
||||
constructor(max = MAX_CACHE_ITEMS) {
|
||||
this._imgElCache = new LRU({
|
||||
ttl: MAX_CACHE_AGE_MS,
|
||||
updateAgeOnGet: true,
|
||||
max,
|
||||
});
|
||||
}
|
||||
|
||||
setDriver(driver) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ImageElement} imgEl
|
||||
* @returns {Element}
|
||||
*/
|
||||
registerImageElement(imgEl) {
|
||||
this.imgElCache.set(imgEl.id, imgEl);
|
||||
const protoKey = this.driver.isW3CProtocol() ? W3C_ELEMENT_KEY : MJSONWP_ELEMENT_KEY;
|
||||
return imgEl.asElement(protoKey);
|
||||
this._imgElCache.set(imgEl.id, imgEl);
|
||||
return imgEl.asElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} imgElId
|
||||
* @returns {ImageElement|undefined}
|
||||
*/
|
||||
getImageElement(imgElId) {
|
||||
return this._imgElCache.get(imgElId);
|
||||
}
|
||||
|
||||
clearImageElements() {
|
||||
this._imgElCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,27 +77,28 @@ export default class ImageElementFinder {
|
||||
* multiple
|
||||
* @property {boolean} [ignoreDefaultImageTemplateScale=false] - Whether we
|
||||
* ignore defaultImageTemplateScale. It can be used when you would like to
|
||||
* scale b64Template with defaultImageTemplateScale setting.
|
||||
* scale template with defaultImageTemplateScale setting.
|
||||
* @property {import('@appium/types').Rect?} containerRect - The bounding
|
||||
* rectangle to limit the search in
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find a screen rect represented by an ImageElement corresponding to an image
|
||||
* template sent in by the client
|
||||
*
|
||||
* @param {string} b64Template - base64-encoded image used as a template to be
|
||||
* @param {Buffer} template - image used as a template to be
|
||||
* matched in the screenshot
|
||||
* @param {ExternalDriver} driver
|
||||
* @param {FindByImageOptions} opts - additional options
|
||||
*
|
||||
* @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
|
||||
*/
|
||||
async findByImage(
|
||||
b64Template,
|
||||
{shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false}
|
||||
template,
|
||||
driver,
|
||||
{shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false, containerRect = null}
|
||||
) {
|
||||
if (!this.driver) {
|
||||
throw new Error(`Can't find without a driver!`);
|
||||
}
|
||||
const settings = {...DEFAULT_SETTINGS, ...this.driver.settings.getSettings()};
|
||||
const settings = {...DEFAULT_SETTINGS, ...driver.settings.getSettings()};
|
||||
const {
|
||||
imageMatchThreshold: threshold,
|
||||
imageMatchMethod,
|
||||
@@ -149,28 +109,38 @@ export default class ImageElementFinder {
|
||||
} = settings;
|
||||
|
||||
log.info(`Finding image element with match threshold ${threshold}`);
|
||||
if (!this.driver.getWindowSize) {
|
||||
throw new Error("This driver does not support the required 'getWindowSize' command");
|
||||
if (!driver.getWindowRect && !driver.getWindowSize) {
|
||||
throw new Error("This driver does not support the required 'getWindowRect' command");
|
||||
}
|
||||
let screenSize;
|
||||
if (driver.getWindowRect) {
|
||||
const screenRect = await driver.getWindowRect();
|
||||
screenSize = {
|
||||
width: screenRect.width,
|
||||
height: screenRect.height,
|
||||
};
|
||||
} else {
|
||||
// TODO: Drop the deprecated endpoint
|
||||
screenSize = await driver.getWindowSize();
|
||||
}
|
||||
const {width: screenWidth, height: screenHeight} = await this.driver.getWindowSize();
|
||||
|
||||
// someone might have sent in a template that's larger than the screen
|
||||
// dimensions. If so let's check and cut it down to size since the algorithm
|
||||
// will not work unless we do. But because it requires some potentially
|
||||
// expensive commands, only do this if the user has requested it in settings.
|
||||
if (fixImageTemplateSize) {
|
||||
b64Template = await this.ensureTemplateSize(b64Template, screenWidth, screenHeight);
|
||||
template = await this.ensureTemplateSize(template, {
|
||||
width: containerRect ? containerRect.width : screenSize.width,
|
||||
height: containerRect ? containerRect.height : screenSize.height,
|
||||
});
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const condition = async () => {
|
||||
try {
|
||||
const {b64Screenshot, scale} = await this.getScreenshotForImageFind(
|
||||
screenWidth,
|
||||
screenHeight
|
||||
);
|
||||
const {screenshot, scale} = await this.getScreenshotForImageFind(driver, screenSize);
|
||||
|
||||
b64Template = await this.fixImageTemplateScale(b64Template, {
|
||||
template = await this.fixImageTemplateScale(template, {
|
||||
defaultImageTemplateScale,
|
||||
ignoreDefaultImageTemplateScale,
|
||||
fixImageTemplateScale,
|
||||
@@ -185,21 +155,21 @@ export default class ImageElementFinder {
|
||||
if (imageMatchMethod) {
|
||||
comparisonOpts.method = imageMatchMethod;
|
||||
}
|
||||
if (multiple) {
|
||||
results.push(
|
||||
...(await compareImages(
|
||||
MATCH_TEMPLATE_MODE,
|
||||
b64Screenshot,
|
||||
b64Template,
|
||||
comparisonOpts
|
||||
))
|
||||
);
|
||||
} else {
|
||||
results.push(
|
||||
await compareImages(MATCH_TEMPLATE_MODE, b64Screenshot, b64Template, comparisonOpts)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
|
||||
const pushIfOk = (el) => {
|
||||
if (containerRect && !containsRect(containerRect, el.rect)) {
|
||||
log.debug(
|
||||
`The matched element rectangle ${JSON.stringify(el.rect)} is not located ` +
|
||||
`inside of the bounding rectangle ${JSON.stringify(containerRect)}, thus rejected`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
results.push(el);
|
||||
return true;
|
||||
};
|
||||
|
||||
const elOrEls = await compareImages(MATCH_TEMPLATE_MODE, screenshot, template, comparisonOpts);
|
||||
return _.some((_.isArray(elOrEls) ? elOrEls : [elOrEls]).map(pushIfOk));
|
||||
} catch (err) {
|
||||
// if compareImages fails, we'll get a specific error, but we should
|
||||
// retry, so trap that and just return false to trigger the next round of
|
||||
@@ -213,7 +183,7 @@ export default class ImageElementFinder {
|
||||
};
|
||||
|
||||
try {
|
||||
await this.driver.implicitWaitForCondition(condition);
|
||||
await driver.implicitWaitForCondition(condition);
|
||||
} catch (err) {
|
||||
// this `implicitWaitForCondition` method will throw a 'Condition unmet'
|
||||
// error if an element is not found eventually. In that case, we will
|
||||
@@ -235,7 +205,14 @@ export default class ImageElementFinder {
|
||||
|
||||
const elements = results.map(({rect, score, visualization}) => {
|
||||
log.info(`Image template matched: ${JSON.stringify(rect)}`);
|
||||
return new ImageElement(b64Template, rect, score, visualization, this);
|
||||
return new ImageElement({
|
||||
template,
|
||||
rect,
|
||||
score,
|
||||
match: visualization ? Buffer.from(visualization, 'base64') : null,
|
||||
finder: this,
|
||||
containerRect,
|
||||
});
|
||||
});
|
||||
|
||||
// if we're just checking staleness, return straightaway so we don't add
|
||||
@@ -253,86 +230,90 @@ export default class ImageElementFinder {
|
||||
/**
|
||||
* Ensure that the image template sent in for a find is of a suitable size
|
||||
*
|
||||
* @param {string} b64Template - base64-encoded image
|
||||
* @param {number} screenWidth - width of screen
|
||||
* @param {number} screenHeight - height of screen
|
||||
* @param {Buffer} template - template image
|
||||
* @param {import('@appium/types').Size} maxSize - size of the bounding rectangle
|
||||
*
|
||||
* @returns {Promise<string>} base64-encoded image, potentially resized
|
||||
* @returns {Promise<Buffer>} image, potentially resized
|
||||
*/
|
||||
async ensureTemplateSize(b64Template, screenWidth, screenHeight) {
|
||||
let imgObj = await imageUtil.getJimpImage(b64Template);
|
||||
let {width: tplWidth, height: tplHeight} = imgObj.bitmap;
|
||||
async ensureTemplateSize(template, maxSize) {
|
||||
const imgObj = sharp(template);
|
||||
const {width: tplWidth, height: tplHeight} = await imgObj.metadata();
|
||||
|
||||
log.info(
|
||||
`Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`
|
||||
`Template image is ${tplWidth}x${tplHeight}. Bounding rectangle size is ${maxSize.width}x${maxSize.height}`
|
||||
);
|
||||
// if the template fits inside the screen dimensions, we're good
|
||||
if (tplWidth <= screenWidth && tplHeight <= screenHeight) {
|
||||
return b64Template;
|
||||
if (tplWidth <= maxSize.width && tplHeight <= maxSize.height) {
|
||||
return template;
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Scaling template image from ${tplWidth}x${tplHeight} to match ` +
|
||||
`screen at ${screenWidth}x${screenHeight}`
|
||||
`the bounding rectangle at ${maxSize.width}x${maxSize.height}`
|
||||
);
|
||||
// otherwise, scale it to fit inside the screen dimensions
|
||||
imgObj = imgObj.scaleToFit(screenWidth, screenHeight);
|
||||
return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
|
||||
// otherwise, scale it to fit inside the bounding rectangle dimensions:
|
||||
// https://sharp.pixelplumbing.com/api-resize
|
||||
return await imgObj.resize({
|
||||
width: parseInt(maxSize.width, 10),
|
||||
height: parseInt(maxSize.height, 10),
|
||||
fit: 'inside',
|
||||
})
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the screenshot image that will be used for find by element, potentially
|
||||
* altering it in various ways based on user-requested settings
|
||||
*
|
||||
* @param {number} screenWidth - width of screen
|
||||
* @param {number} screenHeight - height of screen
|
||||
* @param {ExternalDriver} driver
|
||||
* @param {import('@appium/types').Size} screenSize - The original size of the screen
|
||||
*
|
||||
* @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} base64-encoded screenshot and ScreenshotScale
|
||||
* @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} PNG screenshot and ScreenshotScale
|
||||
*/
|
||||
async getScreenshotForImageFind(screenWidth, screenHeight) {
|
||||
if (!this.driver.getScreenshot) {
|
||||
async getScreenshotForImageFind(driver, screenSize) {
|
||||
if (!driver.getScreenshot) {
|
||||
throw new Error("This driver does not support the required 'getScreenshot' command");
|
||||
}
|
||||
const settings = Object.assign({}, DEFAULT_SETTINGS, this.driver.settings.getSettings());
|
||||
const settings = Object.assign({}, DEFAULT_SETTINGS, driver.settings.getSettings());
|
||||
const {fixImageFindScreenshotDims} = settings;
|
||||
|
||||
let b64Screenshot = await this.driver.getScreenshot();
|
||||
const screenshot = Buffer.from(await driver.getScreenshot(), 'base64');
|
||||
|
||||
// if the user has requested not to correct for aspect or size differences
|
||||
// between the screenshot and the screen, just return the screenshot now
|
||||
if (!fixImageFindScreenshotDims) {
|
||||
log.info(`Not verifying screenshot dimensions match screen`);
|
||||
return {b64Screenshot};
|
||||
return {screenshot};
|
||||
}
|
||||
|
||||
if (screenWidth < 1 || screenHeight < 1) {
|
||||
if (screenSize.width < 1 || screenSize.height < 1) {
|
||||
log.warn(
|
||||
`The retrieved screen size ${screenWidth}x${screenHeight} does ` +
|
||||
`not seem to be valid. No changes will be applied to the screenshot`
|
||||
`The retrieved screen size ${screenSize.width}x${screenSize.height} does ` +
|
||||
`not seem to be valid. No changes will be applied to the screenshot`
|
||||
);
|
||||
return {b64Screenshot};
|
||||
return {screenshot};
|
||||
}
|
||||
|
||||
// otherwise, do some verification on the screenshot to make sure it matches
|
||||
// the screen size and aspect ratio
|
||||
log.info('Verifying screenshot size and aspect ratio');
|
||||
|
||||
let imgObj = await imageUtil.getJimpImage(b64Screenshot);
|
||||
let {width: shotWidth, height: shotHeight} = imgObj.bitmap;
|
||||
let imgObj = sharp(screenshot);
|
||||
let {width: shotWidth, height: shotHeight} = await imgObj.metadata();
|
||||
|
||||
if (shotWidth < 1 || shotHeight < 1) {
|
||||
log.warn(
|
||||
`The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
|
||||
`not seem to be valid. No changes will be applied to the screenshot`
|
||||
`not seem to be valid. No changes will be applied to the screenshot`
|
||||
);
|
||||
return {b64Screenshot};
|
||||
return {screenshot};
|
||||
}
|
||||
|
||||
if (screenWidth === shotWidth && screenHeight === shotHeight) {
|
||||
if (screenSize.width === shotWidth && screenSize.height === shotHeight) {
|
||||
// the height and width of the screenshot and the device screen match, which
|
||||
// means we should be safe when doing template matches
|
||||
log.info('Screenshot size matched screen size');
|
||||
return {b64Screenshot};
|
||||
return {screenshot};
|
||||
}
|
||||
|
||||
// otherwise, if they don't match, it could spell problems for the accuracy
|
||||
@@ -343,19 +324,19 @@ export default class ImageElementFinder {
|
||||
|
||||
const scale = {xScale: 1.0, yScale: 1.0};
|
||||
|
||||
const screenAR = screenWidth / screenHeight;
|
||||
const screenAR = screenSize.width / screenSize.height;
|
||||
const shotAR = shotWidth / shotHeight;
|
||||
if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {
|
||||
log.info(
|
||||
`Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
|
||||
`screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`
|
||||
`screen aspect ratio '${screenAR}' (${screenSize.width}x${screenSize.height})`
|
||||
);
|
||||
} else {
|
||||
log.warn(
|
||||
`When trying to find an element, determined that the screen ` +
|
||||
`aspect ratio and screenshot aspect ratio are different. Screen ` +
|
||||
`is ${screenWidth}x${screenHeight} whereas screenshot is ` +
|
||||
`${shotWidth}x${shotHeight}.`
|
||||
`aspect ratio and screenshot aspect ratio are different. Screen ` +
|
||||
`is ${screenSize.width}x${screenSize.height} whereas screenshot is ` +
|
||||
`${shotWidth}x${shotHeight}.`
|
||||
);
|
||||
|
||||
// In the case where the x-scale and y-scale are different, we need to decide
|
||||
@@ -364,46 +345,56 @@ export default class ImageElementFinder {
|
||||
// this.getScreenshot(shotWidth, shotHeight) is 540x397,
|
||||
// this.getDeviceSize(screenWidth, screenHeight) is 1080x1920.
|
||||
// The ratio would then be {xScale: 0.5, yScale: 0.2}.
|
||||
// In this case, we must should `yScale: 0.2` as scaleFactor, because
|
||||
// In this case, we must use `yScale: 0.2` as scaleFactor, because
|
||||
// if we select the xScale, the height will be bigger than real screenshot size
|
||||
// which is used to image comparison by OpenCV as a base image.
|
||||
// All of this is primarily useful when the screenshot is a horizontal slice taken out of the
|
||||
// screen (for example not including top/bottom nav bars)
|
||||
const xScale = (1.0 * shotWidth) / screenWidth;
|
||||
const yScale = (1.0 * shotHeight) / screenHeight;
|
||||
const scaleFactor = xScale >= yScale ? yScale : xScale;
|
||||
const xScale = (1.0 * shotWidth) / screenSize.width;
|
||||
const yScale = (1.0 * shotHeight) / screenSize.height;
|
||||
const scaleFactor = Math.min(xScale, yScale);
|
||||
const [newWidth, newHeight] = [shotWidth * scaleFactor, shotHeight * scaleFactor]
|
||||
.map((x) => parseInt(x, 10));
|
||||
|
||||
log.warn(
|
||||
`Resizing screenshot to ${shotWidth * scaleFactor}x${shotHeight * scaleFactor} to match ` +
|
||||
`screen aspect ratio so that image element coordinates have a ` +
|
||||
`greater chance of being correct.`
|
||||
`Resizing screenshot to ${newWidth}x${newHeight} to match ` +
|
||||
`screen aspect ratio so that image element coordinates have a ` +
|
||||
`greater chance of being correct.`
|
||||
);
|
||||
imgObj = imgObj.resize(shotWidth * scaleFactor, shotHeight * scaleFactor);
|
||||
imgObj = imgObj.resize({
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
fit: 'fill',
|
||||
});
|
||||
|
||||
scale.xScale *= scaleFactor;
|
||||
scale.yScale *= scaleFactor;
|
||||
|
||||
shotWidth = imgObj.bitmap.width;
|
||||
shotHeight = imgObj.bitmap.height;
|
||||
[shotWidth, shotHeight] = [newWidth, newHeight];
|
||||
}
|
||||
|
||||
// Resize based on the screen dimensions only if both width and height are mismatched
|
||||
// since except for that, it might be a situation which is different window rect and
|
||||
// screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and
|
||||
// `"deviceScreenSize"=>"1080x1920"`
|
||||
if (screenWidth !== shotWidth && screenHeight !== shotHeight) {
|
||||
if (screenSize.width !== shotWidth && screenSize.height !== shotHeight) {
|
||||
log.info(
|
||||
`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
|
||||
`screen at ${screenWidth}x${screenHeight}`
|
||||
`screen at ${screenSize.width}x${screenSize.height}`
|
||||
);
|
||||
imgObj = imgObj.resize(screenWidth, screenHeight);
|
||||
imgObj = imgObj.resize({
|
||||
width: parseInt(screenSize.width, 10),
|
||||
height: parseInt(screenSize.height, 10),
|
||||
fit: 'fill',
|
||||
});
|
||||
|
||||
scale.xScale *= (1.0 * screenWidth) / shotWidth;
|
||||
scale.yScale *= (1.0 * screenHeight) / shotHeight;
|
||||
scale.xScale *= (1.0 * screenSize.width) / shotWidth;
|
||||
scale.yScale *= (1.0 * screenSize.height) / shotHeight;
|
||||
}
|
||||
|
||||
b64Screenshot = (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
|
||||
return {b64Screenshot, scale};
|
||||
return {
|
||||
screenshot: await imgObj.toBuffer(),
|
||||
scale,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,7 +402,7 @@ export default class ImageElementFinder {
|
||||
* @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings
|
||||
* @property {number} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
|
||||
* @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.
|
||||
* If b64Template has been scaled to defaultImageTemplateScale or should ignore the scale,
|
||||
* If the template has been scaled to defaultImageTemplateScale or should ignore the scale,
|
||||
* this parameter should be true. e.g. click in image-element module
|
||||
* @property {number} xScale - Scale ratio for width
|
||||
* @property {number} yScale - Scale ratio for height
|
||||
@@ -421,20 +412,19 @@ export default class ImageElementFinder {
|
||||
* Get a image that will be used for template maching.
|
||||
* Returns scaled image if scale ratio is provided.
|
||||
*
|
||||
*
|
||||
* @param {string} b64Template - base64-encoded image used as a template to be
|
||||
* @param {Buffer} template - image used as a template to be
|
||||
* matched in the screenshot
|
||||
* @param {ImageTemplateSettings} opts - Image template scale related options
|
||||
*
|
||||
* @returns {Promise<string>} base64-encoded scaled template screenshot
|
||||
* @returns {Promise<Buffer>} scaled template screenshot
|
||||
*/
|
||||
async fixImageTemplateScale(b64Template, opts) {
|
||||
async fixImageTemplateScale(template, opts) {
|
||||
if (!opts) {
|
||||
return b64Template;
|
||||
return template;
|
||||
}
|
||||
|
||||
let {
|
||||
fixImageTemplateScale = false,
|
||||
fixImageTemplateScale: fixTplScale = false,
|
||||
defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
ignoreDefaultImageTemplateScale = false,
|
||||
xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
||||
@@ -446,12 +436,12 @@ export default class ImageElementFinder {
|
||||
}
|
||||
|
||||
// Default
|
||||
if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixImageTemplateScale) {
|
||||
return b64Template;
|
||||
if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixTplScale) {
|
||||
return template;
|
||||
}
|
||||
|
||||
// Calculate xScale and yScale Appium should scale
|
||||
if (fixImageTemplateScale) {
|
||||
if (fixTplScale) {
|
||||
xScale *= defaultImageTemplateScale;
|
||||
yScale *= defaultImageTemplateScale;
|
||||
} else {
|
||||
@@ -460,7 +450,7 @@ export default class ImageElementFinder {
|
||||
|
||||
// xScale and yScale can be NaN if defaultImageTemplateScale is string, for example
|
||||
if (!parseFloat(String(xScale)) || !parseFloat(String(yScale))) {
|
||||
return b64Template;
|
||||
return template;
|
||||
}
|
||||
|
||||
// Return if the scale is default, 1, value
|
||||
@@ -474,26 +464,27 @@ export default class ImageElementFinder {
|
||||
)
|
||||
)
|
||||
) {
|
||||
return b64Template;
|
||||
return template;
|
||||
}
|
||||
|
||||
let imgTempObj = await imageUtil.getJimpImage(b64Template);
|
||||
let {width: baseTempWidth, height: baseTempHeigh} = imgTempObj.bitmap;
|
||||
let imgObj = sharp(template);
|
||||
const {width: baseTempWidth, height: baseTempHeigh} = await imgObj.metadata();
|
||||
|
||||
const scaledWidth = baseTempWidth * xScale;
|
||||
const scaledHeight = baseTempHeigh * yScale;
|
||||
log.info(
|
||||
`Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +
|
||||
` to ${scaledWidth}x${scaledHeight}`
|
||||
`Scaling template image from ${baseTempWidth}x${baseTempHeigh} to ${scaledWidth}x${scaledHeight}`
|
||||
);
|
||||
log.info(`The ratio is ${xScale} and ${yScale}`);
|
||||
imgTempObj = await imgTempObj.resize(scaledWidth, scaledHeight);
|
||||
return (await imgTempObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
|
||||
imgObj = imgObj.resize({
|
||||
width: parseInt(scaledWidth, 10),
|
||||
height: parseInt(scaledHeight, 10),
|
||||
fit: 'fill',
|
||||
});
|
||||
return await imgObj.toBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE};
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
||||
* @typedef {import('@appium/types').Element} Element
|
||||
@@ -501,7 +492,7 @@ export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAG
|
||||
|
||||
/**
|
||||
* @typedef Screenshot
|
||||
* @property {string} b64Screenshot - base64 based screenshot string
|
||||
* @property {Buffer} screenshot - screenshot image as PNG
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,14 +2,12 @@ import _ from 'lodash';
|
||||
import {errors} from 'appium/driver';
|
||||
import {util} from 'appium/support';
|
||||
import log from './logger';
|
||||
import {DEFAULT_SETTINGS} from './finder';
|
||||
import {
|
||||
IMAGE_STRATEGY, DEFAULT_SETTINGS, IMAGE_TAP_STRATEGIES,
|
||||
IMAGE_ELEMENT_PREFIX, IMAGE_EL_TAP_STRATEGY_W3C,
|
||||
} from './constants';
|
||||
|
||||
const IMAGE_ELEMENT_PREFIX = 'appium-image-element-';
|
||||
const TAP_DURATION_MS = 125;
|
||||
const IMAGE_EL_TAP_STRATEGY_W3C = 'w3cActions';
|
||||
const IMAGE_EL_TAP_STRATEGY_MJSONWP = 'touchActions';
|
||||
const IMAGE_TAP_STRATEGIES = [IMAGE_EL_TAP_STRATEGY_MJSONWP, IMAGE_EL_TAP_STRATEGY_W3C];
|
||||
const DEFAULT_TEMPLATE_IMAGE_SCALE = 1.0;
|
||||
|
||||
/**
|
||||
* @typedef Dimension
|
||||
@@ -23,28 +21,41 @@ const DEFAULT_TEMPLATE_IMAGE_SCALE = 1.0;
|
||||
* @property {number} y - y coordinate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef ImageElementOpts
|
||||
* @property {Buffer} template - the image which was used to find this ImageElement
|
||||
* @property {Rect} rect - bounds of matched image element
|
||||
* @property {number} score The similarity score as a float number in range [0.0, 1.0].
|
||||
* 1.0 is the highest score (means both images are totally equal).
|
||||
* @property {Buffer?} match - the image which has matched marks. Defaults to null.
|
||||
* @property {import('./finder').default?} finder - the finder we can use to re-check stale elements
|
||||
* @property {import('@appium/types').Rect?} containerRect - The bounding
|
||||
* rectangle to limit the search in
|
||||
*/
|
||||
|
||||
/**
|
||||
* Representation of an "image element", which is simply a set of coordinates
|
||||
* and methods that can be used on that set of coordinates via the driver
|
||||
*/
|
||||
export default class ImageElement {
|
||||
/**
|
||||
* @param {string} b64Template - the base64-encoded image which was used to
|
||||
* find this ImageElement
|
||||
* @param {Rect} rect - bounds of matched image element
|
||||
* @param {number} score The similarity score as a float number in range [0.0, 1.0].
|
||||
* 1.0 is the highest score (means both images are totally equal).
|
||||
* @param {string?} b64Result - the base64-encoded image which has matched marks.
|
||||
* Defaults to null.
|
||||
* @param {import('./finder').default?} finder - the finder we can use to re-check stale elements
|
||||
* @param {ImageElementOpts}
|
||||
*/
|
||||
constructor(b64Template, rect, score, b64Result = null, finder = null) {
|
||||
this.template = b64Template;
|
||||
constructor({
|
||||
template,
|
||||
rect,
|
||||
score,
|
||||
match = null,
|
||||
finder = null,
|
||||
containerRect = null,
|
||||
}) {
|
||||
this.template = template;
|
||||
this.rect = rect;
|
||||
this.id = `${IMAGE_ELEMENT_PREFIX}${util.uuidV4()}`;
|
||||
this.b64MatchedImage = b64Result;
|
||||
this.match = match;
|
||||
this.score = score;
|
||||
this.finder = finder;
|
||||
this.containerRect = containerRect;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,20 +83,25 @@ export default class ImageElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {?string} - the base64-encoded image which has matched marks
|
||||
* @returns {string} - the base64-encoded original image used for matching
|
||||
*/
|
||||
get originalImage() {
|
||||
return this.template.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|null} - the base64-encoded image which has matched marks
|
||||
*/
|
||||
get matchedImage() {
|
||||
return this.b64MatchedImage;
|
||||
return this.match?.toString('base64') ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} protocolKey - the protocol-specific JSON key for
|
||||
* a WebElement
|
||||
*
|
||||
* @returns {Element} - this image element as a WebElement
|
||||
*/
|
||||
asElement(protocolKey) {
|
||||
return {[protocolKey]: this.id};
|
||||
asElement() {
|
||||
return util.wrapElement(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,11 +148,12 @@ export default class ImageElement {
|
||||
if (checkForImageElementStaleness || updatePos) {
|
||||
log.info('Checking image element for staleness before clicking');
|
||||
try {
|
||||
newImgEl = await this.finder.findByImage(this.template, {
|
||||
newImgEl = await this.finder.findByImage(this.template, driver, {
|
||||
shouldCheckStaleness: true,
|
||||
// Set ignoreDefaultImageTemplateScale because this.template is device screenshot based image
|
||||
// managed inside Appium after finidng image by template which managed by a user
|
||||
ignoreDefaultImageTemplateScale: true,
|
||||
containerRect: this.containerRect,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new errors.StaleElementReferenceError();
|
||||
@@ -209,6 +226,26 @@ export default class ImageElement {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform lookup of image element(s) inside of the current element
|
||||
*
|
||||
* @param {boolean} multiple - Whether to lookup multiple elements
|
||||
* @param {import('appium/driver').BaseDriver} driver - The driver to use for commands
|
||||
* @param {string[]} args = Rest of arguments for executeScripts
|
||||
* @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
|
||||
*/
|
||||
async find(multiple, driver, ...args) {
|
||||
const [strategy, selector] = args;
|
||||
if (strategy !== IMAGE_STRATEGY) {
|
||||
throw new errors.InvalidSelectorError(`Lookup strategies other than '${IMAGE_STRATEGY}' are not supported`);
|
||||
}
|
||||
return await this.finder.findByImage(
|
||||
Buffer.from(selector, 'base64'),
|
||||
driver,
|
||||
{multiple, containerRect: this.rect}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle various Appium commands that involve an image element
|
||||
*
|
||||
@@ -223,6 +260,10 @@ export default class ImageElement {
|
||||
switch (cmd) {
|
||||
case 'click':
|
||||
return await imgEl.click(driver);
|
||||
case 'findElementFromElement':
|
||||
return await imgEl.find(false, driver, ...args);
|
||||
case 'findElementsFromElement':
|
||||
return await imgEl.find(true, driver, ...args);
|
||||
case 'elementDisplayed':
|
||||
return true;
|
||||
case 'getSize':
|
||||
@@ -232,6 +273,8 @@ export default class ImageElement {
|
||||
return imgEl.location;
|
||||
case 'getElementRect':
|
||||
return imgEl.rect;
|
||||
case 'getElementScreenshot':
|
||||
return imgEl.originalImage;
|
||||
case 'getAttribute':
|
||||
// /session/:sessionId/element/:elementId/attribute/:name
|
||||
// /session/:sessionId/element/:elementId/attribute/visual should retun the visual data
|
||||
@@ -250,13 +293,7 @@ export default class ImageElement {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ImageElement,
|
||||
IMAGE_EL_TAP_STRATEGY_MJSONWP,
|
||||
IMAGE_EL_TAP_STRATEGY_W3C,
|
||||
DEFAULT_TEMPLATE_IMAGE_SCALE,
|
||||
IMAGE_ELEMENT_PREFIX,
|
||||
};
|
||||
export {ImageElement};
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').Rect} Rect
|
||||
|
||||
@@ -5,16 +5,11 @@ import {errors} from 'appium/driver';
|
||||
import BasePlugin from 'appium/plugin';
|
||||
import {compareImages} from './compare';
|
||||
import ImageElementFinder from './finder';
|
||||
import {ImageElement, IMAGE_ELEMENT_PREFIX} from './image-element';
|
||||
|
||||
const IMAGE_STRATEGY = '-image';
|
||||
import {ImageElement} from './image-element';
|
||||
import {IMAGE_STRATEGY, IMAGE_ELEMENT_PREFIX} from './constants';
|
||||
|
||||
function getImgElFromArgs(args) {
|
||||
for (let arg of args) {
|
||||
if (_.isString(arg) && arg.startsWith(IMAGE_ELEMENT_PREFIX)) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return args.find((arg) => _.isString(arg) && arg.startsWith(IMAGE_ELEMENT_PREFIX));
|
||||
}
|
||||
|
||||
export default class ImageElementPlugin extends BasePlugin {
|
||||
@@ -57,8 +52,7 @@ export default class ImageElementPlugin extends BasePlugin {
|
||||
return await next();
|
||||
}
|
||||
|
||||
this.finder.setDriver(driver);
|
||||
return await this.finder.findByImage(selector, {multiple});
|
||||
return await this.finder.findByImage(Buffer.from(selector, 'base64'), driver, {multiple});
|
||||
}
|
||||
|
||||
async handle(next, driver, cmdName, ...args) {
|
||||
@@ -66,13 +60,17 @@ export default class ImageElementPlugin extends BasePlugin {
|
||||
// and execute the command on it
|
||||
const imgElId = getImgElFromArgs(args);
|
||||
if (imgElId) {
|
||||
if (!this.finder.imgElCache.has(imgElId)) {
|
||||
const imgEl = this.finder.getImageElement(imgElId);
|
||||
if (!imgEl) {
|
||||
throw new errors.NoSuchElementError();
|
||||
}
|
||||
const imgEl = this.finder.imgElCache.get(imgElId);
|
||||
return await ImageElement.execute(driver, imgEl, cmdName, ...args);
|
||||
}
|
||||
|
||||
if (cmdName === 'deleteSession') {
|
||||
this.finder.clearImageElements();
|
||||
}
|
||||
|
||||
// otherwise just do the normal thing
|
||||
return await next();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/images-plugin",
|
||||
"version": "2.0.10",
|
||||
"version": "2.1.2",
|
||||
"description": "Plugin for working with images and image elements in Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -39,9 +39,10 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/opencv": "^2.0.10",
|
||||
"@appium/opencv": "^2.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "7.18.3",
|
||||
"sharp": "0.32.1",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE} from '../../lib/compare';
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE} from '../../lib/constants';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures';
|
||||
import {pluginE2EHarness} from '@appium/plugin-test-support';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
||||
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
|
||||
@@ -32,11 +35,14 @@ const WDIO_OPTS = {
|
||||
};
|
||||
|
||||
describe('ImageElementPlugin', function () {
|
||||
let server,
|
||||
driver = null;
|
||||
let server;
|
||||
let driver;
|
||||
|
||||
// this hook is intended to be run before the hooks created by `e2eSetup`
|
||||
after(async function () {
|
||||
beforeEach(async function () {
|
||||
driver = await wdio(WDIO_OPTS);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
}
|
||||
@@ -58,7 +64,6 @@ describe('ImageElementPlugin', function () {
|
||||
});
|
||||
|
||||
it('should add the compareImages route', async function () {
|
||||
driver = await wdio(WDIO_OPTS);
|
||||
let comparison = await driver.compareImages(
|
||||
MATCH_FEATURES_MODE,
|
||||
TEST_IMG_1_B64,
|
||||
@@ -85,4 +90,29 @@ describe('ImageElementPlugin', function () {
|
||||
height.should.eql(91);
|
||||
await imageEl.click();
|
||||
});
|
||||
|
||||
it('should find subelements', async function () {
|
||||
const imageEl = await driver.$(APPSTORE_IMG_PATH);
|
||||
const {width, height} = await imageEl.getSize();
|
||||
const tmpRoot = await tempDir.openDir();
|
||||
try {
|
||||
const screenshotPath = path.join(tmpRoot, 'element.png');
|
||||
await imageEl.saveScreenshot(screenshotPath);
|
||||
const tmpImgPath = path.join(tmpRoot, 'region.png');
|
||||
await sharp(screenshotPath)
|
||||
.extract(
|
||||
{
|
||||
left: parseInt(width / 4, 10),
|
||||
top: parseInt(height / 4, 10),
|
||||
width: parseInt(width / 2, 10),
|
||||
height: parseInt(height / 2, 10),
|
||||
}
|
||||
)
|
||||
.toFile(tmpImgPath);
|
||||
const subEl = await imageEl.$(tmpImgPath);
|
||||
_.isNil(subEl).should.be.false;
|
||||
} finally {
|
||||
await fs.rimraf(tmpRoot);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user