mirror of
https://github.com/appium/appium.git
synced 2026-11-03 15:56:15 +08:00
Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
591d15bc5e | ||
|
|
00dbd28190 | ||
|
|
b808dc866d | ||
|
|
d866bddc15 | ||
|
|
825833d1ad | ||
|
|
bb11d1cfe9 |
@@ -147,3 +147,8 @@ labels:
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/universal-xml-plugin/**']
|
||||
|
||||
- label: '@appium/strongbox'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/strongbox/**']
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
name: Appium Build
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
# this list of paths could potentially impact the build
|
||||
paths:
|
||||
- '**/tsconfig*.json'
|
||||
- 'packages/base-driver/static/**/*'
|
||||
- 'packages/*/test/**'
|
||||
- 'packages/*/bin'
|
||||
- 'packages/**/*.ts'
|
||||
- 'packages/**/*.json'
|
||||
- 'packages/**/*.js'
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/tsconfig*.json'
|
||||
- 'packages/base-driver/static/**/*'
|
||||
- 'packages/*/test/**'
|
||||
- 'packages/*/bin'
|
||||
- 'packages/**/*.ts'
|
||||
- 'packages/**/*.json'
|
||||
- 'packages/**/*.js'
|
||||
- 'package*.json'
|
||||
- 'lerna.json'
|
||||
- '.npmrc'
|
||||
- '.mocharc.js'
|
||||
- '.github/workflows/build.yml' # this file
|
||||
- '.eslintrc'
|
||||
- '.eslintignore'
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,7 +50,7 @@ jobs:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
|
||||
with:
|
||||
@@ -33,7 +70,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:
|
||||
@@ -41,7 +78,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: |
|
||||
|
||||
+29
-18
@@ -2,30 +2,46 @@ name: Build Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- closed
|
||||
paths:
|
||||
- './packages/appium/docs/**'
|
||||
- './packages/docutils/**'
|
||||
- './packages/typedoc-plugin-appium/**'
|
||||
|
||||
concurrency: preview-${{ github.ref }}
|
||||
- '**/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/**'
|
||||
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
|
||||
- name: Install dependencies (node)
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
- name: Install dependencies (Node.js)
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
useRollingCache: true
|
||||
install-command: npm ci
|
||||
- name: Install dependencies (python)
|
||||
- name: Install dependencies (Python)
|
||||
run: pip install -r packages/docutils/requirements.txt
|
||||
- name: Configure Git User
|
||||
run: |
|
||||
@@ -33,8 +49,3 @@ jobs:
|
||||
git config --local user.name "github-actions[bot]"
|
||||
- name: Build Docs
|
||||
run: npm run docs:preview
|
||||
- name: Deploy Preview
|
||||
if: ${{ github.repository == 'appium/appium' }}
|
||||
uses: rossjrw/pr-preview-action@9dac5c4777c535516ebf819f93aeadac70f66488 # v1
|
||||
with:
|
||||
source-dir: ./packages/appium/docs/site
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
+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
|
||||
|
||||
|
||||
+10
-5
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/lerna",
|
||||
"changelogPreset": "conventional-changelog-conventionalcommits",
|
||||
"command": {
|
||||
"add": {
|
||||
"exact": true
|
||||
},
|
||||
"publish": {
|
||||
"message": "chore: publish"
|
||||
},
|
||||
"run": {
|
||||
"concurrency": 8
|
||||
},
|
||||
@@ -9,13 +17,10 @@
|
||||
"version": {
|
||||
"allowBranch": "master"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
"exact": true
|
||||
}
|
||||
},
|
||||
"ignoreChanges": ["**/test/**", "**/*.md"],
|
||||
"version": "independent",
|
||||
"useNx": false,
|
||||
"useWorkspaces": true,
|
||||
"useNx": false
|
||||
"version": "independent"
|
||||
}
|
||||
|
||||
Generated
+20011
-2166
File diff suppressed because it is too large
Load Diff
+18
-15
@@ -57,6 +57,7 @@
|
||||
"rebuild": "run-s clean build",
|
||||
"reinstall": "run-s clean clean:deps ci",
|
||||
"reinstall:angry": "npm run clean:dangerously && npm ci",
|
||||
"release": "lerna publish",
|
||||
"stage-synced-pkgs": "git add -A packages/appium/README.md \"packages/*/package.json\"",
|
||||
"start": "appium",
|
||||
"sync-pkgs": "run-p sync-pkgs:*",
|
||||
@@ -84,31 +85,32 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"@commitlint/cli": "17.5.1",
|
||||
"@commitlint/config-conventional": "17.4.4",
|
||||
"@commitlint/cli": "17.6.3",
|
||||
"@commitlint/config-conventional": "17.6.3",
|
||||
"@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/node": "18.16.5",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/sinon": "10.0.14",
|
||||
"@types/sinon-chai": "3.2.9",
|
||||
"@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.2",
|
||||
"@typescript-eslint/parser": "5.59.2",
|
||||
"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.40.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,19 +120,20 @@
|
||||
"handlebars": "4.7.7",
|
||||
"husky": "8.0.3",
|
||||
"json-schema-to-typescript": "12.0.0",
|
||||
"lerna": "6.6.1",
|
||||
"lint-staged": "13.2.0",
|
||||
"lerna": "6.6.2",
|
||||
"lint-staged": "13.2.2",
|
||||
"log-symbols": "4.1.0",
|
||||
"midnight-smoker": "2.0.1",
|
||||
"midnight-smoker": "2.0.3",
|
||||
"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",
|
||||
"serve-static": "1.15.0",
|
||||
"sinon": "15.0.3",
|
||||
"sinon": "15.0.4",
|
||||
"sinon-chai": "3.7.0",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"through2": "4.0.2",
|
||||
@@ -141,7 +144,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,79 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.0-beta.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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0-beta.60](https://github.com/appium/appium/compare/appium@2.0.0-beta.59...appium@2.0.0-beta.60) (2023-04-04)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
+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
|
||||
|
||||
@@ -2,4 +2,93 @@
|
||||
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 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 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.
|
||||
|
||||
Luckily, 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 can then be included in your docs site.
|
||||
|
||||
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).
|
||||
|
||||
### Setup
|
||||
|
||||
To take advantage of Appium's documentation utilities, you'll need to make sure of the following:
|
||||
|
||||
- Python 3 is available
|
||||
- The appropriate Python dependencies are available (the versions used by Appium itself are listed
|
||||
- in the doc utility's
|
||||
[requirements.txt](https://github.com/appium/appium/blob/master/packages/docutils/requirements.txt)).
|
||||
It's recommended you use the same versions.
|
||||
- Include the `@appium/docutils` NPM package as a dev dependency of your project
|
||||
- Annotate your commands using TypeDoc (this works for JS projects and not just TypeScript
|
||||
projects).
|
||||
- Put your markdown docs files in a directory
|
||||
- Create a [`mkdocs.yml`](https://www.mkdocs.org/user-guide/configuration/) file that extends
|
||||
Appium's base MkDocs configuration. (If your `mkdocs.yml` file is in the root of your repo, then
|
||||
the line at the top of it should look like:
|
||||
```yml
|
||||
INHERIT: ./node_modules/@appium/docutils/base-mkdocs.yml
|
||||
```
|
||||
Basically, make sure you're inheriting the path to `@appium/docutils`'s `base-mkdocs.yml`.
|
||||
|
||||
|
||||
### 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
|
||||
appium-docs build
|
||||
|
||||
# Same as build, but host the docs on a local dev server
|
||||
# and watch for changes and rebuild when files change
|
||||
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!
|
||||
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';
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
isPluginCommandArgs,
|
||||
isServerCommandArgs,
|
||||
} from './utils';
|
||||
import os from 'node:os';
|
||||
|
||||
const {resolveAppiumHome} = env;
|
||||
|
||||
@@ -138,6 +139,52 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and validates appium home path folder
|
||||
*
|
||||
* @param {string} name The name of the appium home source (needed for error messages)
|
||||
* @param {string} appiumHome The actual value to be verified
|
||||
* @returns {Promise<string>} Same appiumHome value
|
||||
* @throws {Error} If the validation has failed
|
||||
*/
|
||||
async function prepareAppiumHome(name, appiumHome) {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(appiumHome);
|
||||
} catch (e) {
|
||||
let err = e;
|
||||
if (e.code === 'ENOENT') {
|
||||
try {
|
||||
await fs.mkdir(appiumHome, {recursive: true});
|
||||
return appiumHome;
|
||||
} catch (e1) {
|
||||
err = e1;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`The path '${appiumHome}' provided in the ${name} must point ` +
|
||||
`to a valid folder writeable for the current user account '${os.userInfo().username}'. ` +
|
||||
`Original error: ${err.message}`
|
||||
);
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(
|
||||
`The path '${appiumHome}' provided in the ${name} must point to a valid folder`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await fs.access(appiumHome, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`The folder path '${appiumHome}' provided in the ${name} must be ` +
|
||||
`writeable for the current user account '${os.userInfo().username}. ` +
|
||||
`Original error: ${e.message}`
|
||||
);
|
||||
}
|
||||
return appiumHome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Appium, but does not start the server.
|
||||
*
|
||||
@@ -157,6 +204,13 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
|
||||
*/
|
||||
async function init(args) {
|
||||
const appiumHome = args?.appiumHome ?? (await resolveAppiumHome());
|
||||
let appiumHomeSourceName = 'autodetected appium home path';
|
||||
if (!_.isNil(args?.appiumHome)) {
|
||||
appiumHomeSourceName = 'appiumHome config value';
|
||||
} else if (process.env.APPIUM_HOME) {
|
||||
appiumHomeSourceName = 'APPIUM_HOME environment variable';
|
||||
}
|
||||
await prepareAppiumHome(appiumHomeSourceName, appiumHome);
|
||||
|
||||
adjustNodePath();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.0.0-beta.60",
|
||||
"version": "2.0.0-beta.67",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -60,12 +60,12 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.3.5",
|
||||
"@appium/base-plugin": "^2.2.5",
|
||||
"@appium/docutils": "^0.3.3",
|
||||
"@appium/base-driver": "^9.3.8",
|
||||
"@appium/base-plugin": "^2.2.8",
|
||||
"@appium/docutils": "^0.3.10",
|
||||
"@appium/schema": "^0.2.6",
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/types": "^0.10.3",
|
||||
"@appium/support": "^4.0.0",
|
||||
"@appium/types": "^0.11.1",
|
||||
"@sidvind/better-ajv-errors": "2.1.0",
|
||||
"@types/argparse": "2.0.10",
|
||||
"@types/bluebird": "3.5.38",
|
||||
@@ -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.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.7.2",
|
||||
"type-fest": "3.10.0",
|
||||
"winston": "3.8.2",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.2.1"
|
||||
"yaml": "2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "7.30.2"
|
||||
"webdriverio": "7.31.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,14 @@ describe('parser', function () {
|
||||
const args = p.parseArgs([DRIVER_TYPE, 'list', '--updates']);
|
||||
args.showUpdates.should.eql(true);
|
||||
});
|
||||
it('should allow "ls" as an alias for "list"', function () {
|
||||
const args = p.parseArgs([DRIVER_TYPE, 'ls']);
|
||||
args.subcommand.should.eql(DRIVER_TYPE);
|
||||
args.driverCommand.should.eql('list');
|
||||
args.showInstalled.should.eql(false);
|
||||
args.showUpdates.should.eql(false);
|
||||
args.json.should.eql(false);
|
||||
});
|
||||
});
|
||||
describe('install', function () {
|
||||
it('should not allow an empty argument list', function () {
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.8",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,17 +44,17 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/types": "^0.10.3",
|
||||
"@appium/support": "^4.0.0",
|
||||
"@appium/types": "^0.11.1",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/async-lock": "1.4.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/express": "4.17.17",
|
||||
"@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 +66,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "3.7.2",
|
||||
"type-fest": "3.10.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.8",
|
||||
"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.8",
|
||||
"@appium/support": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,41 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/doctor",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.15",
|
||||
"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.0",
|
||||
"@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.2",
|
||||
"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",
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.3](https://github.com/appium/appium/compare/@appium/docutils@0.3.2...@appium/docutils@0.3.3) (2023-04-04)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readPackageJson, whichMike} from '../fs';
|
||||
import logger from '../logger';
|
||||
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`
|
||||
@@ -124,6 +124,7 @@ export async function deploy({
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mikeArgs, serveOpts);
|
||||
} else {
|
||||
log.info('Deploying into branch %s', branch);
|
||||
const mikeArgs = [
|
||||
...argify(
|
||||
_.omitBy(
|
||||
@@ -138,7 +139,7 @@ export async function deploy({
|
||||
}
|
||||
await doDeploy(mikeArgs, execOpts);
|
||||
|
||||
log.success('Mike finished deployment into branch %s (%dms)', branch, stop());
|
||||
log.success('Finished deployment into branch %s (%dms)', branch, stop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ import {
|
||||
safeWriteFile,
|
||||
stringifyYaml,
|
||||
} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
import {MkDocsYml, MkDocsYmlNav} from '../model';
|
||||
import {relative} from '../util';
|
||||
|
||||
const log = logger.withTag('builder:nav');
|
||||
const log = getLogger('builder:nav');
|
||||
|
||||
/**
|
||||
* Gets a list of `.md` files relative to `docs_dir`
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findTypeDocJsonPath, readTypedocJson} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
import {argify, relative, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('builder:reference');
|
||||
const log = getLogger('builder:reference');
|
||||
|
||||
/**
|
||||
* Executes TypeDoc _in the current process_
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
* @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_YML} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, readMkDocsYml, whichMkDocs} from '../fs';
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
import {relative, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
const log = logger.withTag('mkdocs');
|
||||
const log = getLogger('mkdocs');
|
||||
|
||||
/**
|
||||
* Runs `mkdocs serve`
|
||||
@@ -58,7 +57,6 @@ async function doBuild(
|
||||
export async function buildSite({
|
||||
mkdocsYml: mkDocsYmlPath,
|
||||
siteDir,
|
||||
theme = NAME_THEME,
|
||||
cwd = process.cwd(),
|
||||
serve = false,
|
||||
serveOpts,
|
||||
@@ -73,7 +71,7 @@ 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);
|
||||
}
|
||||
@@ -81,16 +79,26 @@ export async function buildSite({
|
||||
// unsure about how SIGHUP is handled here
|
||||
await doServe(mkdocsArgs, serveOpts);
|
||||
} else {
|
||||
log.info('Building site...');
|
||||
await doBuild(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
|
||||
|
||||
@@ -7,11 +7,11 @@ import path from 'node:path';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {buildReferenceDocs, buildSite, deploy, updateNav} from '../../builder';
|
||||
import {NAME_BIN} from '../../constants';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {stopwatch} from '../../util';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('build');
|
||||
const log = getLogger('build');
|
||||
|
||||
enum BuildCommandGroup {
|
||||
Build = 'Build Config:',
|
||||
@@ -220,6 +220,7 @@ export default {
|
||||
);
|
||||
},
|
||||
async handler(args) {
|
||||
log.info('Building docs...');
|
||||
const stop = stopwatch('build');
|
||||
log.debug('Build command called with args: %O', args);
|
||||
if (!args.site && !args.reference) {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
import _ from 'lodash';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {init} from '../../init';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {stopwatch} from '../../util';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const log = getLogger('init');
|
||||
|
||||
enum InitCommandGroup {
|
||||
MkDocs = 'MkDocs Config:',
|
||||
|
||||
@@ -7,10 +7,10 @@ import {util} from '@appium/support';
|
||||
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
|
||||
import {DocutilsError} from '../../error';
|
||||
import {DocutilsValidator, ValidationKind} from '../../validate';
|
||||
import logger from '../../logger';
|
||||
import {getLogger} from '../../logger';
|
||||
import {checkMissingPaths} from '../check';
|
||||
|
||||
const log = logger.withTag('validate');
|
||||
const log = getLogger('validate');
|
||||
|
||||
enum ValidateCommandGroup {
|
||||
Behavior = 'Validation Behavior:',
|
||||
|
||||
@@ -11,10 +11,11 @@ import path from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
import parser from 'yargs-parser';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import {NAME_BIN} from '../constants';
|
||||
import logger from '../logger';
|
||||
import {DEFAULT_LOG_LEVEL, LogLevelMap, NAME_BIN} from '../constants';
|
||||
import {getLogger, initLogger, isLogLevelString} from '../logger';
|
||||
import {relative} from '../util';
|
||||
const log = logger.withTag('config');
|
||||
|
||||
const log = getLogger('config');
|
||||
|
||||
/**
|
||||
* `lilconfig` loader for YAML
|
||||
@@ -43,8 +44,19 @@ const loadEsm: Loader = (filepath: string) => import(filepath);
|
||||
export async function findConfig(argv: string[] = hideBin(process.argv)) {
|
||||
const preArgs = parser(argv);
|
||||
|
||||
if (preArgs.verbose || preArgs.logLevel === 'debug') {
|
||||
log.level = LogLevel.Debug;
|
||||
// if --verbose is used, set the log level to debug.
|
||||
// otherwise use --log-level or the default.
|
||||
let logLevel: LogLevel | keyof typeof LogLevelMap;
|
||||
if (preArgs.verbose) {
|
||||
logLevel = LogLevel.Debug;
|
||||
} else {
|
||||
// if the loglevel is valid, use it, otherwise use the default
|
||||
logLevel = isLogLevelString(preArgs.logLevel) ? preArgs.logLevel : DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
initLogger(logLevel);
|
||||
|
||||
if (preArgs.noConfig) {
|
||||
log.debug('Not loading config because --no-config was provided');
|
||||
}
|
||||
|
||||
return preArgs.noConfig || preArgs.help || preArgs.version
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import {getLogger} from '../logger';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
@@ -20,7 +20,7 @@ import {sync as readPkg} from 'read-pkg';
|
||||
|
||||
const pkg = readPkg({cwd: fs.findRoot(__dirname)});
|
||||
|
||||
const log = logger.withTag('cli');
|
||||
const log = getLogger('cli');
|
||||
export async function main(argv = hideBin(process.argv)) {
|
||||
const config = await findConfig(argv);
|
||||
|
||||
@@ -56,24 +56,14 @@ export async function main(argv = hideBin(process.argv)) {
|
||||
describe: 'Disable config file discovery',
|
||||
},
|
||||
})
|
||||
.middleware([
|
||||
.middleware(
|
||||
/**
|
||||
* Configures logging; `--verbose` implies `--log-level=debug`
|
||||
* Writes a startup message
|
||||
*/
|
||||
(argv) => {
|
||||
if (argv.verbose) {
|
||||
argv.logLevel = 'debug';
|
||||
log.debug('Debug logging enabled via --verbose');
|
||||
}
|
||||
log.level = LogLevelMap[argv.logLevel];
|
||||
},
|
||||
/**
|
||||
* Writes a startup message, if logging is enabled
|
||||
*/
|
||||
async () => {
|
||||
() => {
|
||||
log.info(`${pkg.name} @ v${pkg.version} (Node.js ${process.version})`);
|
||||
},
|
||||
])
|
||||
}
|
||||
)
|
||||
.epilog(`Please report bugs at ${pkg.bugs?.url}`)
|
||||
.fail(
|
||||
/**
|
||||
@@ -85,8 +75,7 @@ export async function main(argv = hideBin(process.argv)) {
|
||||
log.error(error.message);
|
||||
} else {
|
||||
y.showHelp();
|
||||
console.log();
|
||||
log.error(msg ?? error.message);
|
||||
log.error(`\n\n${msg ?? error.message}`);
|
||||
}
|
||||
y.exit(1, error);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -12,7 +12,7 @@ import {fs} from '@appium/support';
|
||||
import * as JSON5 from 'json5';
|
||||
import _ from 'lodash';
|
||||
import _pkgDir from 'pkg-dir';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {Application, TypeDocReader} from 'typedoc';
|
||||
import {
|
||||
NAME_TYPEDOC_JSON,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import {DocutilsError} from './error';
|
||||
import {MkDocsYml} from './model';
|
||||
|
||||
const log = logger.withTag('fs');
|
||||
const log = getLogger('fs');
|
||||
|
||||
/**
|
||||
* Finds path to closest `package.json`
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './builder';
|
||||
export * from './validate';
|
||||
export * from './scaffold';
|
||||
export * from './constants';
|
||||
export * from './logger';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {exec} from 'teen_process';
|
||||
import {Simplify} from 'type-fest';
|
||||
import {DocutilsError} from './error';
|
||||
import {createScaffoldTask, ScaffoldTaskOptions} from './scaffold';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, TsConfigJson, TypeDocJson} from './model';
|
||||
import _ from 'lodash';
|
||||
import {stringifyJson5, stringifyYaml} from './fs';
|
||||
@@ -54,8 +54,8 @@ const BASE_TSCONFIG_JSON: Readonly<TsConfigJson> = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
const log = logger.withTag('init');
|
||||
const dryRunLog = log.withTag('dry-run');
|
||||
const log = getLogger('init');
|
||||
const dryRunLog = getLogger('dry-run', log);
|
||||
|
||||
const DEFAULT_INCLUDE = ['lib', 'test', 'index.js'];
|
||||
/**
|
||||
|
||||
@@ -137,57 +137,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
|
||||
|
||||
@@ -59,17 +59,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
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
whichPython,
|
||||
readMkDocsYml,
|
||||
} from './fs';
|
||||
import logger from './logger';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, PipPackage, TypeDocJson} from './model';
|
||||
import {relative} from './util';
|
||||
|
||||
@@ -64,7 +64,7 @@ const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
|
||||
*/
|
||||
const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
|
||||
|
||||
const log = logger.withTag('validate');
|
||||
const log = getLogger('validate');
|
||||
|
||||
/**
|
||||
* The "kinds" of validation which were requested to be performed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.10",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -49,9 +49,9 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.1.9",
|
||||
"@appium/tsconfig": "^0.2.4",
|
||||
"@appium/typedoc-plugin-appium": "^0.5.4",
|
||||
"@appium/support": "^4.0.0",
|
||||
"@appium/tsconfig": "^0.3.0",
|
||||
"@appium/typedoc-plugin-appium": "^0.6.4",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "2.15.3",
|
||||
@@ -64,16 +64,16 @@
|
||||
"log-symbols": "4.1.0",
|
||||
"pkg-dir": "5.0.0",
|
||||
"read-pkg": "5.2.0",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.7.2",
|
||||
"type-fest": "3.10.0",
|
||||
"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.2.2",
|
||||
"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.9
|
||||
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,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.6",
|
||||
"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.11.1",
|
||||
"@types/lodash": "4.14.194",
|
||||
"@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.0.4",
|
||||
"source-map-support": "0.5.21",
|
||||
"stoppable": "1.1.0",
|
||||
"type-fest": "3.7.2"
|
||||
"type-fest": "3.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.13",
|
||||
"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.17",
|
||||
"webdriverio": "7.31.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35"
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.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.12",
|
||||
"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.8",
|
||||
"@appium/support": "^4.0.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.9](https://github.com/appium/appium/compare/@appium/images-plugin@2.0.8...@appium/images-plugin@2.0.9) (2023-04-03)
|
||||
|
||||
**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,22 @@ 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(selector, driver, {multiple, containerRect: this.rect});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle various Appium commands that involve an image element
|
||||
*
|
||||
@@ -223,6 +256,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 +269,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 +289,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(selector, 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.9",
|
||||
"version": "2.1.0",
|
||||
"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.9",
|
||||
"@appium/opencv": "^2.1.0",
|
||||
"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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+5
-2
@@ -2,6 +2,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const TINY_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQwIDc5LjE2MDQ1MSwgMjAxNy8wNS8wNi0wMTowODoyMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6N0NDMDM4MDM4N0U2MTFFOEEzMzhGMTRFNUUwNzIwNUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6N0NDMDM4MDQ4N0U2MTFFOEEzMzhGMTRFNUUwNzIwNUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3Q0MwMzgwMTg3RTYxMUU4QTMzOEYxNEU1RTA3MjA1QiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3Q0MwMzgwMjg3RTYxMUU4QTMzOEYxNEU1RTA3MjA1QiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpdvJjQAAAAlSURBVHjaJInBEQAACIKw/Xe2Ul5wYBtwmJqkk4+zfvUQVoABAEg0EfrZwc0hAAAAAElFTkSuQmCC';
|
||||
const TiNY_PNG_BUF = Buffer.from(TINY_PNG, 'base64');
|
||||
const TINY_PNG_DIMS = [4, 4];
|
||||
|
||||
const TEST_IMG_1_PATH = path.resolve(__dirname, 'img1.png');
|
||||
@@ -13,5 +14,7 @@ const TEST_IMG_2_B64 = fs.readFileSync(TEST_IMG_2_PATH).toString('base64');
|
||||
const TEST_IMG_2_PART_B64 = fs.readFileSync(TEST_IMG_2_PART_PATH).toString('base64');
|
||||
|
||||
|
||||
export { TINY_PNG, TINY_PNG_DIMS, TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64,
|
||||
APPSTORE_IMG_PATH };
|
||||
export {
|
||||
TINY_PNG, TINY_PNG_DIMS, TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64,
|
||||
APPSTORE_IMG_PATH, TiNY_PNG_BUF,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import _ from 'lodash';
|
||||
import {imageUtil} from 'appium/support';
|
||||
import {util} from 'appium/support';
|
||||
import {BaseDriver} from 'appium/driver';
|
||||
import {ImageElementPlugin, IMAGE_STRATEGY} from '../../lib/plugin';
|
||||
import {ImageElementPlugin} from '../../lib/plugin';
|
||||
import {IMAGE_STRATEGY} from '../../lib/constants';
|
||||
import ImageElementFinder from '../../lib/finder';
|
||||
import ImageElement from '../../lib/image-element';
|
||||
import sinon from 'sinon';
|
||||
import {TINY_PNG, TINY_PNG_DIMS} from '../fixtures';
|
||||
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const compareModule = require('../../lib/compare');
|
||||
|
||||
const plugin = new ImageElementPlugin();
|
||||
|
||||
class PluginDriver extends BaseDriver {
|
||||
async getWindowSize() {}
|
||||
async getWindowRect() {}
|
||||
async getScreenshot() {}
|
||||
findElement(strategy, selector) {
|
||||
return plugin.findElement(_.noop, this, strategy, selector);
|
||||
@@ -56,8 +58,8 @@ describe('finding elements by image', function () {
|
||||
const rect = {x: 10, y: 20, width: 30, height: 40};
|
||||
const score = 0.9;
|
||||
const size = {width: 100, height: 200};
|
||||
const screenshot = 'iVBORfoo';
|
||||
const template = 'iVBORbar';
|
||||
const screenshot = Buffer.from('iVBORfoo', 'base64');
|
||||
const template = Buffer.from('iVBORbar', 'base64');
|
||||
/** @type {PluginDriver} */
|
||||
let d;
|
||||
/** @type {ImageElementFinder} */
|
||||
@@ -66,15 +68,18 @@ describe('finding elements by image', function () {
|
||||
let compareStub;
|
||||
|
||||
function basicStub(driver, finder) {
|
||||
const sizeStub = sandbox.stub(driver, 'getWindowSize').returns(size);
|
||||
const rectStub = sandbox.stub(driver, 'getWindowRect').returns({
|
||||
x: 0,
|
||||
y: 0,
|
||||
...size,
|
||||
});
|
||||
const screenStub = sandbox.stub(finder, 'getScreenshotForImageFind').returns(screenshot);
|
||||
return {sizeStub, screenStub};
|
||||
return {rectStub, screenStub};
|
||||
}
|
||||
|
||||
function basicImgElVerify(imgElProto, finder) {
|
||||
const imgElId = imgElProto.ELEMENT;
|
||||
finder.imgElCache.has(imgElId).should.be.true;
|
||||
const imgEl = finder.imgElCache.get(imgElId);
|
||||
const imgElId = util.unwrapElement(imgElProto);
|
||||
const imgEl = finder.getImageElement(imgElId);
|
||||
(imgEl instanceof ImageElement).should.be.true;
|
||||
imgEl.rect.should.eql(rect);
|
||||
imgEl.score.should.eql(score);
|
||||
@@ -83,7 +88,7 @@ describe('finding elements by image', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
d = new PluginDriver();
|
||||
f = new ImageElementFinder(d);
|
||||
f = new ImageElementFinder();
|
||||
compareStub = sandbox;
|
||||
compareStub = sandbox.stub(compareModule, 'compareImages');
|
||||
compareStub.resolves({rect, score});
|
||||
@@ -91,201 +96,194 @@ describe('finding elements by image', function () {
|
||||
});
|
||||
|
||||
it('should find an image element happypath', async function () {
|
||||
const imgElProto = await f.findByImage(template, {multiple: false});
|
||||
const imgElProto = await f.findByImage(template, d, {multiple: false});
|
||||
basicImgElVerify(imgElProto, f);
|
||||
});
|
||||
it('should find image elements happypath', async function () {
|
||||
compareStub.resolves([{rect, score}]);
|
||||
const els = await f.findByImage(template, {multiple: true});
|
||||
const els = await f.findByImage(template, d, {multiple: true});
|
||||
els.should.have.length(1);
|
||||
basicImgElVerify(els[0], f);
|
||||
});
|
||||
it('should fail if driver does not support getWindowSize', async function () {
|
||||
d.getWindowSize = null;
|
||||
it('should fail if driver does not support getWindowRect', async function () {
|
||||
d.getWindowRect = null;
|
||||
await f
|
||||
.findByImage(template, {multiple: false})
|
||||
.findByImage(template, d, {multiple: false})
|
||||
.should.eventually.be.rejectedWith(/driver does not support/);
|
||||
});
|
||||
it('should fix template size if requested', async function () {
|
||||
const newTemplate = 'iVBORbaz';
|
||||
const newTemplateBuf = Buffer.from(newTemplate, 'base64');
|
||||
await d.settings.update({fixImageTemplateSize: true});
|
||||
sandbox.stub(f, 'ensureTemplateSize').returns(newTemplate);
|
||||
const imgElProto = await f.findByImage(template, {multiple: false});
|
||||
sandbox.stub(f, 'ensureTemplateSize').returns(newTemplateBuf);
|
||||
const imgElProto = await f.findByImage(template, d, {multiple: false});
|
||||
const imgEl = basicImgElVerify(imgElProto, f);
|
||||
imgEl.template.should.eql(newTemplate);
|
||||
_.last(compareStub.args)[2].should.eql(newTemplate);
|
||||
imgEl.originalImage.should.eql(newTemplate);
|
||||
_.last(compareStub.args)[2].should.eql(newTemplateBuf);
|
||||
});
|
||||
|
||||
it('should fix template size scale if requested', async function () {
|
||||
const newTemplate = 'iVBORbaz';
|
||||
const newTemplateBuf = Buffer.from(newTemplate, 'base64');
|
||||
await d.settings.update({fixImageTemplateScale: true});
|
||||
sandbox.stub(f, 'fixImageTemplateScale').returns(newTemplate);
|
||||
const imgElProto = await f.findByImage(template, {multiple: false});
|
||||
sandbox.stub(f, 'fixImageTemplateScale').returns(newTemplateBuf);
|
||||
const imgElProto = await f.findByImage(template, d, {multiple: false});
|
||||
const imgEl = basicImgElVerify(imgElProto, f);
|
||||
imgEl.template.should.eql(newTemplate);
|
||||
_.last(compareStub.args)[2].should.eql(newTemplate);
|
||||
imgEl.originalImage.should.eql(newTemplate);
|
||||
_.last(compareStub.args)[2].should.eql(newTemplateBuf);
|
||||
});
|
||||
it('should not fix template size scale if it is not requested', async function () {
|
||||
const newTemplate = 'iVBORbaz';
|
||||
const newTemplateBuf = Buffer.from(newTemplate, 'base64');
|
||||
await d.settings.update({});
|
||||
sandbox.stub(f, 'fixImageTemplateScale').returns(newTemplate);
|
||||
sandbox.stub(f, 'fixImageTemplateScale').returns(newTemplateBuf);
|
||||
f.fixImageTemplateScale.callCount.should.eql(0);
|
||||
});
|
||||
|
||||
it('should throw an error if template match fails', async function () {
|
||||
compareStub.rejects(new Error('Cannot find any occurrences'));
|
||||
await f
|
||||
.findByImage(template, {multiple: false})
|
||||
.findByImage(template, d, {multiple: false})
|
||||
.should.be.rejectedWith(/element could not be located/);
|
||||
});
|
||||
it('should return empty array for multiple elements if template match fails', async function () {
|
||||
compareStub.rejects(new Error('Cannot find any occurrences'));
|
||||
await f.findByImage(template, {multiple: true}).should.eventually.eql([]);
|
||||
await f.findByImage(template, d, {multiple: true}).should.eventually.eql([]);
|
||||
});
|
||||
it('should respect implicit wait', async function () {
|
||||
d.setImplicitWait(10);
|
||||
compareStub.resetHistory();
|
||||
compareStub.returns({rect, score});
|
||||
compareStub.onFirstCall().throws(new Error('Cannot find any occurrences'));
|
||||
const imgElProto = await f.findByImage(template, {multiple: false});
|
||||
const imgElProto = await f.findByImage(template, d, {multiple: false});
|
||||
basicImgElVerify(imgElProto, f);
|
||||
compareStub.should.have.been.calledTwice;
|
||||
});
|
||||
it('should not add element to cache and return it directly when checking staleness', async function () {
|
||||
const imgEl = await f.findByImage(template, {
|
||||
const imgEl = await f.findByImage(template, d, {
|
||||
multiple: false,
|
||||
shouldCheckStaleness: true,
|
||||
});
|
||||
(imgEl instanceof ImageElement).should.be.true;
|
||||
f.imgElCache.has(imgEl.id).should.be.false;
|
||||
_.isNil(f.getImageElement(imgEl.id)).should.be.true;
|
||||
imgEl.rect.should.eql(rect);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixImageTemplateScale', function () {
|
||||
let d;
|
||||
let f;
|
||||
const basicTemplate = 'iVBORbaz';
|
||||
const basicTemplateBuf = Buffer.from(basicTemplate, 'base64');
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
d = new PluginDriver();
|
||||
f = new ImageElementFinder(d);
|
||||
f = new ImageElementFinder();
|
||||
});
|
||||
|
||||
it('should not fix template size scale if no scale value', async function () {
|
||||
await f
|
||||
.fixImageTemplateScale(basicTemplate, {fixImageTemplateScale: true})
|
||||
.should.eventually.eql(basicTemplate);
|
||||
.fixImageTemplateScale(basicTemplateBuf, {fixImageTemplateScale: true})
|
||||
.should.eventually.eql(basicTemplateBuf);
|
||||
});
|
||||
|
||||
it('should not fix template size scale if it is null', async function () {
|
||||
await f.fixImageTemplateScale(basicTemplate, null).should.eventually.eql(basicTemplate);
|
||||
await f.fixImageTemplateScale(basicTemplateBuf, null).should.eventually.eql(basicTemplateBuf);
|
||||
});
|
||||
|
||||
it('should not fix template size scale if it is not number', async function () {
|
||||
await f
|
||||
.fixImageTemplateScale(basicTemplate, 'wrong-scale')
|
||||
.should.eventually.eql(basicTemplate);
|
||||
.fixImageTemplateScale(basicTemplateBuf, 'wrong-scale')
|
||||
.should.eventually.eql(basicTemplateBuf);
|
||||
});
|
||||
|
||||
it('should fix template size scale', async function () {
|
||||
const actual =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAWElEQVR4AU3BQRWAQAhAwa/PGBsEgrC16AFBKEIPXW7OXO+Rmey9iQjMjHFzrLUwM7qbqmLcHKpKRFBVuDvj4agq3B1VRUQYT2bS3QwRQVUZF/CaGRHB3wc1vSZbHO5+BgAAAABJRU5ErkJggg==';
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
fixImageTemplateScale: true,
|
||||
xScale: 1.5,
|
||||
yScale: 1.5,
|
||||
})
|
||||
.should.eventually.eql(actual);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should not fix template size scale because of fixImageTemplateScale being false', async function () {
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
fixImageTemplateScale: false,
|
||||
xScale: 1.5,
|
||||
yScale: 1.5,
|
||||
})
|
||||
.should.eventually.eql(TINY_PNG);
|
||||
.should.eventually.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should fix template size scale with default scale', async function () {
|
||||
const actual =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABwUlEQVR4AaXBPUsrQQCG0SeX+cBdkTjwTpG1NPgLpjY/fW1stt4UYmm2cJqwMCsaw70uJJ3CBc9Z/P3Cl+12S9u2tG1L27bEGLm/v2ez2bDZbJDEd/7wS4YT7z3X19fc3Nxwd3dHXdd47xnHkefnZ8ZxpKoq6rqmqiqMMcwMJ1VV0TQN0zThnOPj44O6rsk503UdkmiahqZpWK1WGGOYGU7quqZpGqy1SCLnTM6Z19dXcs5IYpomrLVI4uLigpnhpKoqVqsVkjgcDjw9PdF1HTlnuq5DEs45JHE4HDgznByPR97e3pimiVIK4zhyPB7x3hNCIITA5eUl3nsWiwVnhpNSCsMwsNvtGIaB/X5PKQVJpJSQxHq9RhLOOc4MJ9M0sdvt2G639H3PTBIxRiQhCUnEGLHWcmY4KaUwDAN93/P4+MhyuSSlhCRSSkjCOYe1FmstZ6bve2YvLy/s93tmy+USSUhCEpIIIfAd8/DwwOz9/Z1SCpJIKSGJ9XqNJJxz/MS0bcvs6uoKScQYkYQkJBFjxFrLT0zbtsxub29JKSGJlBKScM5hrcVay09MzplZjJHPz0+894QQCCHwP/7wS/8A4e6nAg+R8LwAAAAASUVORK5CYII=';
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
defaultImageTemplateScale: 4.0,
|
||||
})
|
||||
.should.eventually.eql(actual);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should fix template size scale with default scale and image scale', async function () {
|
||||
const actual =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACaUlEQVR4AbXBMWvrWBSF0c9BsFPtW91UR1U6+///FKlKKt8qqnyqnMozggkI8xgMj6x1uv+L/6zryrIsrOvKsiys68qyLFwuF87nM5fLhfP5zOVy4Xw+84wXftkLv2ziQBK26b0TEVQVu4jANrvM5Hq9spOEJCQhCUlI4mjiQBK26b1TVewkYRvb7DKTMQaZiW1s01rDNraRxNHEgSRaa1QVO0m01jjKTDKTXe+d3jtVxU4SjyYOJGGbnSRs03snM8lMMpPb7UZmkplEBFXFThK2eTRxIAnbSMI2VcX39zdjDMYYZCaZyRiDMQZVxU4StqkqHk0cSEISf5KZ7DKTMQbLsrCTRGuN3jtVxaOJg6qiqqgqqoqqoqoYY5CZ7GwTEdzvd97f34kIeu/YRhKPJg6qiswkM7ndbmQmmUlmkpnsbBMR2CYimOeZ3ju2kcSjiYOqIjP5+vpi2za2bWPbNo5aa7TW2PXe6b3Te6e1hiQeTRxUFbfbjW3bGGNwvV4ZY2Ab27TWsI1tbGMb27TWsI0kHk0cVBWZybZtXK9XPj8/+fj4YJ5nIoLWGraJCOZ5RhKSkIQkJPFo4qCqyEy2bWOMwefnJ+u6cjqdsM3ONvM8cz6feca0ris/rtcrmcnONhHB/X7n/f2diKD3jm0k8axpWRZ+ZCaZyc42EYFtIoJ5num9YxtJPGta15U/sY1tdm9vb/Te6b1jG0k8a1qWhR+2sU1rjdYatrGNbWxjm9YaknjWtK4rPyKCiKC1hm0igojg9fUVSUhCEpJ41rQsC0e22dkmIrhcLvyNF/7H6XTib73wy174Zf8AJEsePtlPj10AAAAASUVORK5CYII=';
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
defaultImageTemplateScale: 4.0,
|
||||
fixImageTemplateScale: true,
|
||||
xScale: 1.5,
|
||||
yScale: 1.5,
|
||||
})
|
||||
.should.eventually.eql(actual);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should not fix template size scale with default scale and image scale', async function () {
|
||||
const actual =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABwUlEQVR4AaXBPUsrQQCG0SeX+cBdkTjwTpG1NPgLpjY/fW1stt4UYmm2cJqwMCsaw70uJJ3CBc9Z/P3Cl+12S9u2tG1L27bEGLm/v2ez2bDZbJDEd/7wS4YT7z3X19fc3Nxwd3dHXdd47xnHkefnZ8ZxpKoq6rqmqiqMMcwMJ1VV0TQN0zThnOPj44O6rsk503UdkmiahqZpWK1WGGOYGU7quqZpGqy1SCLnTM6Z19dXcs5IYpomrLVI4uLigpnhpKoqVqsVkjgcDjw9PdF1HTlnuq5DEs45JHE4HDgznByPR97e3pimiVIK4zhyPB7x3hNCIITA5eUl3nsWiwVnhpNSCsMwsNvtGIaB/X5PKQVJpJSQxHq9RhLOOc4MJ9M0sdvt2G639H3PTBIxRiQhCUnEGLHWcmY4KaUwDAN93/P4+MhyuSSlhCRSSkjCOYe1FmstZ6bve2YvLy/s93tmy+USSUhCEpIIIfAd8/DwwOz9/Z1SCpJIKSGJ9XqNJJxz/MS0bcvs6uoKScQYkYQkJBFjxFrLT0zbtsxub29JKSGJlBKScM5hrcVay09MzplZjJHPz0+894QQCCHwP/7wS/8A4e6nAg+R8LwAAAAASUVORK5CYII=';
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
defaultImageTemplateScale: 4.0,
|
||||
fixImageTemplateScale: false,
|
||||
xScale: 1.5,
|
||||
yScale: 1.5,
|
||||
})
|
||||
.should.eventually.eql(actual);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should not fix template size scale because of ignoreDefaultImageTemplateScale', async function () {
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
defaultImageTemplateScale: 4.0,
|
||||
ignoreDefaultImageTemplateScale: true,
|
||||
})
|
||||
.should.eventually.eql(TINY_PNG);
|
||||
.should.eventually.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
|
||||
it('should ignore defaultImageTemplateScale to fix template size scale because of ignoreDefaultImageTemplateScale', async function () {
|
||||
const actual =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAWElEQVR4AU3BQRWAQAhAwa/PGBsEgrC16AFBKEIPXW7OXO+Rmey9iQjMjHFzrLUwM7qbqmLcHKpKRFBVuDvj4agq3B1VRUQYT2bS3QwRQVUZF/CaGRHB3wc1vSZbHO5+BgAAAABJRU5ErkJggg==';
|
||||
await f
|
||||
.fixImageTemplateScale(TINY_PNG, {
|
||||
.fixImageTemplateScale(TiNY_PNG_BUF, {
|
||||
defaultImageTemplateScale: 4.0,
|
||||
ignoreDefaultImageTemplateScale: true,
|
||||
fixImageTemplateScale: true,
|
||||
xScale: 1.5,
|
||||
yScale: 1.5,
|
||||
})
|
||||
.should.eventually.eql(actual);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureTemplateSize', function () {
|
||||
const d = new PluginDriver();
|
||||
const f = new ImageElementFinder(d);
|
||||
const f = new ImageElementFinder();
|
||||
|
||||
it('should not resize the template if it is smaller than the screen', async function () {
|
||||
const screen = TINY_PNG_DIMS.map((n) => n * 2);
|
||||
await f.ensureTemplateSize(TINY_PNG, ...screen).should.eventually.eql(TINY_PNG);
|
||||
const [width, height] = TINY_PNG_DIMS.map((n) => n * 2);
|
||||
await f.ensureTemplateSize(TiNY_PNG_BUF, {width, height}).should.eventually.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
it('should not resize the template if it is the same size as the screen', async function () {
|
||||
await f.ensureTemplateSize(TINY_PNG, ...TINY_PNG_DIMS).should.eventually.eql(TINY_PNG);
|
||||
const [width, height] = TINY_PNG_DIMS;
|
||||
await f.ensureTemplateSize(TiNY_PNG_BUF, {width, height}).should.eventually.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
it('should resize the template if it is bigger than the screen', async function () {
|
||||
const screen = TINY_PNG_DIMS.map((n) => n / 2);
|
||||
const newTemplate = await f.ensureTemplateSize(TINY_PNG, ...screen);
|
||||
newTemplate.should.not.eql(TINY_PNG);
|
||||
newTemplate.length.should.be.below(TINY_PNG.length);
|
||||
const [width, height] = TINY_PNG_DIMS.map((n) => n / 2);
|
||||
const newTemplateBuf = await f.ensureTemplateSize(TiNY_PNG_BUF, {width, height});
|
||||
newTemplateBuf.should.not.eql(TiNY_PNG_BUF);
|
||||
newTemplateBuf.length.should.be.below(TiNY_PNG_BUF.length);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -295,107 +293,100 @@ describe('finding elements by image', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
d = new PluginDriver();
|
||||
f = new ImageElementFinder(d);
|
||||
f = new ImageElementFinder();
|
||||
sandbox.stub(d, 'getScreenshot').returns(TINY_PNG);
|
||||
});
|
||||
|
||||
it('should fail if driver does not support getScreenshot', async function () {
|
||||
const d = new BaseDriver();
|
||||
const f = new ImageElementFinder(d);
|
||||
await f
|
||||
.getScreenshotForImageFind()
|
||||
await new ImageElementFinder()
|
||||
.getScreenshotForImageFind(new BaseDriver())
|
||||
.should.eventually.be.rejectedWith(/driver does not support/);
|
||||
});
|
||||
it('should not adjust or verify screenshot if asked not to by settings', async function () {
|
||||
await d.settings.update({fixImageFindScreenshotDims: false});
|
||||
const screen = TINY_PNG_DIMS.map((n) => n + 1);
|
||||
const {b64Screenshot, scale} = await f.getScreenshotForImageFind(...screen);
|
||||
b64Screenshot.should.eql(TINY_PNG);
|
||||
const [width, height] = TINY_PNG_DIMS.map((n) => n + 1);
|
||||
const {screenshot, scale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
screenshot.should.eql(TiNY_PNG_BUF);
|
||||
should.equal(scale, undefined);
|
||||
});
|
||||
it('should return screenshot without adjustment if it matches screen size', async function () {
|
||||
const {b64Screenshot, scale} = await f.getScreenshotForImageFind(...TINY_PNG_DIMS);
|
||||
b64Screenshot.should.eql(TINY_PNG);
|
||||
const [width, height] = TINY_PNG_DIMS;
|
||||
const {screenshot, scale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
screenshot.should.eql(TiNY_PNG_BUF);
|
||||
should.equal(scale, undefined);
|
||||
});
|
||||
it('should return scaled screenshot with same aspect ratio if matching screen aspect ratio', async function () {
|
||||
const screen = TINY_PNG_DIMS.map((n) => n * 1.5);
|
||||
const {b64Screenshot, scale} = await f.getScreenshotForImageFind(...screen);
|
||||
b64Screenshot.should.not.eql(TINY_PNG);
|
||||
const screenshotObj = await imageUtil.getJimpImage(b64Screenshot);
|
||||
screenshotObj.bitmap.width.should.eql(screen[0]);
|
||||
screenshotObj.bitmap.height.should.eql(screen[1]);
|
||||
const [width, height] = TINY_PNG_DIMS.map((n) => n * 1.5);
|
||||
const {screenshot, scale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
screenshot.should.not.eql(TiNY_PNG_BUF);
|
||||
const screenshotObj = sharp(screenshot);
|
||||
const {width: screenWidth, height: screenHeight} = await screenshotObj.metadata();
|
||||
screenWidth.should.eql(width);
|
||||
screenHeight.should.eql(height);
|
||||
scale.should.eql({xScale: 1.5, yScale: 1.5});
|
||||
});
|
||||
it('should return scaled screenshot with different aspect ratio if not matching screen aspect ratio', async function () {
|
||||
// try first with portrait screen, screen = 8 x 12
|
||||
let screen = [TINY_PNG_DIMS[0] * 2, TINY_PNG_DIMS[1] * 3];
|
||||
let [width, height] = [TINY_PNG_DIMS[0] * 2, TINY_PNG_DIMS[1] * 3];
|
||||
let expectedScale = {xScale: 2.67, yScale: 4};
|
||||
|
||||
const {b64Screenshot, scale} = await f.getScreenshotForImageFind(...screen);
|
||||
b64Screenshot.should.not.eql(TINY_PNG);
|
||||
let screenshotObj = await imageUtil.getJimpImage(b64Screenshot);
|
||||
screenshotObj.bitmap.width.should.eql(screen[0]);
|
||||
screenshotObj.bitmap.height.should.eql(screen[1]);
|
||||
const {screenshot, scale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
screenshot.should.not.eql(TiNY_PNG_BUF);
|
||||
let screenshotObj = sharp(screenshot);
|
||||
let {width: screenWidth, height: screenHeight} = await screenshotObj.metadata();
|
||||
screenWidth.should.eql(width);
|
||||
screenHeight.should.eql(height);
|
||||
scale.xScale.toFixed(2).should.eql(expectedScale.xScale.toString());
|
||||
scale.yScale.should.eql(expectedScale.yScale);
|
||||
|
||||
// then with landscape screen, screen = 12 x 8
|
||||
screen = [TINY_PNG_DIMS[0] * 3, TINY_PNG_DIMS[1] * 2];
|
||||
[width, height] = [TINY_PNG_DIMS[0] * 3, TINY_PNG_DIMS[1] * 2];
|
||||
expectedScale = {xScale: 4, yScale: 2.67};
|
||||
|
||||
const {b64Screenshot: newScreen, scale: newScale} = await f.getScreenshotForImageFind(
|
||||
...screen
|
||||
);
|
||||
newScreen.should.not.eql(TINY_PNG);
|
||||
screenshotObj = await imageUtil.getJimpImage(newScreen);
|
||||
screenshotObj.bitmap.width.should.eql(screen[0]);
|
||||
screenshotObj.bitmap.height.should.eql(screen[1]);
|
||||
const {screenshot: newScreen, scale: newScale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
newScreen.should.not.eql(TiNY_PNG_BUF);
|
||||
screenshotObj = sharp(newScreen);
|
||||
({width: screenWidth, height: screenHeight} = await screenshotObj.metadata());
|
||||
screenWidth.should.eql(width);
|
||||
screenHeight.should.eql(height);
|
||||
newScale.xScale.should.eql(expectedScale.xScale);
|
||||
newScale.yScale.toFixed(2).should.eql(expectedScale.yScale.toString());
|
||||
});
|
||||
|
||||
it('should return scaled screenshot with different aspect ratio if not matching screen aspect ratio with fixImageTemplateScale', async function () {
|
||||
// try first with portrait screen, screen = 8 x 12
|
||||
let screen = [TINY_PNG_DIMS[0] * 2, TINY_PNG_DIMS[1] * 3];
|
||||
let [width, height] = [TINY_PNG_DIMS[0] * 2, TINY_PNG_DIMS[1] * 3];
|
||||
let expectedScale = {xScale: 2.67, yScale: 4};
|
||||
|
||||
const {b64Screenshot, scale} = await f.getScreenshotForImageFind(...screen);
|
||||
b64Screenshot.should.not.eql(TINY_PNG);
|
||||
let screenshotObj = await imageUtil.getJimpImage(b64Screenshot);
|
||||
screenshotObj.bitmap.width.should.eql(screen[0]);
|
||||
screenshotObj.bitmap.height.should.eql(screen[1]);
|
||||
const {screenshot, scale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
screenshot.should.not.eql(TiNY_PNG_BUF);
|
||||
let screenshotObj = sharp(screenshot);
|
||||
let {width: screenWidth, height: screenHeight} = await screenshotObj.metadata();
|
||||
screenWidth.should.eql(width);
|
||||
screenHeight.should.eql(height);
|
||||
scale.xScale.toFixed(2).should.eql(expectedScale.xScale.toString());
|
||||
scale.yScale.should.eql(expectedScale.yScale);
|
||||
// 8 x 12 stretched TINY_PNG
|
||||
await f
|
||||
.fixImageTemplateScale(b64Screenshot, {
|
||||
fixImageTemplateScale: true,
|
||||
scale,
|
||||
})
|
||||
.should.eventually.eql(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAMCAYAAABfnvydAAAAJ0lEQVR4AYXBAQEAIACDMKR/p0fTBrKdbZcPCRIkSJAgQYIECRIkPAzBA1TpeNwZAAAAAElFTkSuQmCC'
|
||||
);
|
||||
.fixImageTemplateScale(screenshot, {fixImageTemplateScale: true, scale})
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
|
||||
// then with landscape screen, screen = 12 x 8
|
||||
screen = [TINY_PNG_DIMS[0] * 3, TINY_PNG_DIMS[1] * 2];
|
||||
[width, height] = [TINY_PNG_DIMS[0] * 3, TINY_PNG_DIMS[1] * 2];
|
||||
expectedScale = {xScale: 4, yScale: 2.67};
|
||||
|
||||
const {b64Screenshot: newScreen, scale: newScale} = await f.getScreenshotForImageFind(
|
||||
...screen
|
||||
);
|
||||
newScreen.should.not.eql(TINY_PNG);
|
||||
screenshotObj = await imageUtil.getJimpImage(newScreen);
|
||||
screenshotObj.bitmap.width.should.eql(screen[0]);
|
||||
screenshotObj.bitmap.height.should.eql(screen[1]);
|
||||
const {screenshot: newScreen, scale: newScale} = await f.getScreenshotForImageFind(d, {width, height});
|
||||
newScreen.should.not.eql(TiNY_PNG_BUF);
|
||||
screenshotObj = sharp(newScreen);
|
||||
({width: screenWidth, height: screenHeight} = await screenshotObj.metadata());
|
||||
screenWidth.should.eql(width);
|
||||
screenHeight.should.eql(height);
|
||||
newScale.xScale.should.eql(expectedScale.xScale);
|
||||
newScale.yScale.toFixed(2).should.eql(expectedScale.yScale.toString());
|
||||
// 12 x 8 stretched TINY_PNG
|
||||
await f
|
||||
.fixImageTemplateScale(newScreen, {fixImageTemplateScale: true, scale})
|
||||
.should.eventually.eql(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAwAAAAICAYAAADN5B7xAAAAI0lEQVR4AZXBAQEAMAyDMI5/T5W2ayB5245AIokkkkgiiST6+W4DTLyo5PUAAAAASUVORK5CYII='
|
||||
);
|
||||
.should.eventually.not.eql(TiNY_PNG_BUF);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import BaseDriver from 'appium/driver';
|
||||
import {util} from 'appium/support';
|
||||
import ImageElementFinder from '../../lib/finder';
|
||||
import {getImgElFromArgs} from '../../lib/plugin';
|
||||
import ImageElement, {IMAGE_ELEMENT_PREFIX} from '../../lib/image-element';
|
||||
import ImageElement from '../../lib/image-element';
|
||||
import sinon from 'sinon';
|
||||
import {IMAGE_ELEMENT_PREFIX} from '../../lib/constants';
|
||||
|
||||
const defRect = {x: 100, y: 110, width: 50, height: 25};
|
||||
const defTemplate = 'iVBORasdf';
|
||||
const defTemplate = Buffer.from('iVBORasdf', 'base64');
|
||||
|
||||
describe('ImageElement', function () {
|
||||
const driver = new BaseDriver();
|
||||
@@ -23,21 +25,30 @@ describe('ImageElement', function () {
|
||||
|
||||
describe('.size', function () {
|
||||
it('should return the width and height of the image el', function () {
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
el.size.should.eql({width: defRect.width, height: defRect.height});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.location', function () {
|
||||
it('should return the location of the image el', function () {
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
el.location.should.eql({x: defRect.x, y: defRect.y});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.center', function () {
|
||||
it('should return the center location of the image el', function () {
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
el.center.should.eql({
|
||||
x: defRect.x + defRect.width / 2,
|
||||
y: defRect.y + defRect.height / 2,
|
||||
@@ -47,21 +58,36 @@ describe('ImageElement', function () {
|
||||
|
||||
describe('.asElement', function () {
|
||||
it('should get the webdriver object representation of the element', function () {
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
el.asElement('ELEMENT').ELEMENT.should.match(/^appium-image-el/);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
util.unwrapElement(el.asElement()).should.match(/^appium-image-el/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.equals', function () {
|
||||
it('should say two image elements with same rect are equal', function () {
|
||||
const el1 = new ImageElement('foo', defRect);
|
||||
const el2 = new ImageElement('bar', defRect);
|
||||
const el1 = new ImageElement({
|
||||
template: Buffer.from('foo'),
|
||||
rect: defRect
|
||||
});
|
||||
const el2 = new ImageElement({
|
||||
template: Buffer.from('bar'),
|
||||
rect: defRect
|
||||
});
|
||||
el1.equals(el2).should.be.true;
|
||||
el2.equals(el1).should.be.true;
|
||||
});
|
||||
it('should say two image elements with different rect are not equal', function () {
|
||||
const el1 = new ImageElement(defTemplate, {...defRect, x: 0});
|
||||
const el2 = new ImageElement(defTemplate, defRect);
|
||||
const el1 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: {...defRect, x: 0}
|
||||
});
|
||||
const el2 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
el1.equals(el2).should.be.false;
|
||||
el2.equals(el1).should.be.false;
|
||||
});
|
||||
@@ -70,15 +96,22 @@ describe('ImageElement', function () {
|
||||
describe('.click', function () {
|
||||
it('should reject an invalid tap strategy', async function () {
|
||||
const d = new BaseDriver();
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
await d.settings.update({imageElementTapStrategy: 'bad'});
|
||||
await el.click(d).should.be.rejectedWith(/Incorrect imageElementTapStrategy/);
|
||||
});
|
||||
it('should try to check for image element staleness, and throw if stale', async function () {
|
||||
const d = new BaseDriver();
|
||||
const f = new ImageElementFinder(d);
|
||||
const f = new ImageElementFinder();
|
||||
sandbox.stub(f, 'findByImage').throws();
|
||||
const el = new ImageElement(defTemplate, defRect, null, null, f);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
finder: f,
|
||||
});
|
||||
// we need to check for staleness if explicitly requested to do so
|
||||
await d.settings.update({
|
||||
checkForImageElementStaleness: true,
|
||||
@@ -97,10 +130,18 @@ describe('ImageElement', function () {
|
||||
const d = new BaseDriver();
|
||||
d.performActions = _.noop;
|
||||
sandbox.stub(d, 'performActions');
|
||||
const f = new ImageElementFinder(d);
|
||||
const el = new ImageElement(defTemplate, defRect, null, null, f);
|
||||
const f = new ImageElementFinder();
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
finder: f,
|
||||
});
|
||||
const newRect = {...defRect, x: defRect.x + 10, y: defRect.y + 5};
|
||||
const elPos2 = new ImageElement(defTemplate, newRect, null, null, f);
|
||||
const elPos2 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: newRect,
|
||||
finder: f,
|
||||
});
|
||||
sandbox.stub(f, 'findByImage').returns(elPos2);
|
||||
await d.settings.update({
|
||||
autoUpdateImageElementPosition: true,
|
||||
@@ -113,7 +154,10 @@ describe('ImageElement', function () {
|
||||
const d = new BaseDriver();
|
||||
d.performActions = _.noop;
|
||||
const actionStub = sandbox.stub(d, 'performActions');
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
// skip the staleness check for this test
|
||||
await d.settings.update({
|
||||
checkForImageElementStaleness: false,
|
||||
@@ -127,7 +171,10 @@ describe('ImageElement', function () {
|
||||
const d = new BaseDriver();
|
||||
d.performTouch = _.noop;
|
||||
const actionStub = sandbox.stub(d, 'performTouch');
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
// skip the staleness check for this test
|
||||
await d.settings.update({
|
||||
checkForImageElementStaleness: false,
|
||||
@@ -143,7 +190,10 @@ describe('ImageElement', function () {
|
||||
const w3cStub = sandbox.stub(d, 'performActions');
|
||||
d.performTouch = _.noop;
|
||||
const touchStub = sandbox.stub(d, 'performTouch');
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
// skip the staleness check for this test
|
||||
await d.settings.update({
|
||||
checkForImageElementStaleness: false,
|
||||
@@ -157,7 +207,10 @@ describe('ImageElement', function () {
|
||||
});
|
||||
it('should throw if driver does not implement any type of action', async function () {
|
||||
const d = new BaseDriver();
|
||||
const el = new ImageElement(defTemplate, defRect);
|
||||
const el = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
// skip the staleness check for this test
|
||||
await d.settings.update({
|
||||
checkForImageElementStaleness: false,
|
||||
@@ -168,20 +221,22 @@ describe('ImageElement', function () {
|
||||
|
||||
describe('#execute', function () {
|
||||
// aGFwcHkgdGVzdGluZw== is 'happy testing'
|
||||
const f = new ImageElementFinder(driver);
|
||||
const imgEl = new ImageElement(defTemplate, defRect, 0, 'aGFwcHkgdGVzdGluZw==', f);
|
||||
const f = new ImageElementFinder();
|
||||
const imgEl = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
score: 0,
|
||||
match: Buffer.from('aGFwcHkgdGVzdGluZw==', 'base64'),
|
||||
finder: f,
|
||||
});
|
||||
let clickStub;
|
||||
|
||||
before(function () {
|
||||
clickStub = sandbox.stub(imgEl, 'click');
|
||||
f.imgElCache.set(imgEl.id, imgEl);
|
||||
f.registerImageElement(imgEl);
|
||||
clickStub.returns(true);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
f.imgElCache.clear();
|
||||
});
|
||||
|
||||
it('should reject executions for unsupported commands', async function () {
|
||||
await ImageElement.execute(driver, imgEl, 'foobar').should.be.rejectedWith(
|
||||
/not yet been implemented/
|
||||
@@ -220,7 +275,10 @@ describe('ImageElement', function () {
|
||||
);
|
||||
});
|
||||
it('should get null as visual of element by default', async function () {
|
||||
const imgElement = new ImageElement(defTemplate, defRect);
|
||||
const imgElement = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
await ImageElement.execute(
|
||||
driver,
|
||||
imgElement,
|
||||
@@ -244,24 +302,34 @@ describe('ImageElement', function () {
|
||||
|
||||
describe('image element LRU cache', function () {
|
||||
it('should accept and cache image elements', function () {
|
||||
const el1 = new ImageElement(defTemplate, defRect);
|
||||
const el2 = new ImageElement(defTemplate, defRect);
|
||||
const cache = new ImageElementFinder().imgElCache;
|
||||
cache.set(el1.id, el1);
|
||||
el1.equals(cache.get(el1.id)).should.be.true;
|
||||
_.isUndefined(cache.get(el2.id)).should.be.true;
|
||||
cache.has(el1.id).should.be.true;
|
||||
cache.has(el2.id).should.be.false;
|
||||
const el1 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
const el2 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
const finder = new ImageElementFinder();
|
||||
finder.registerImageElement(el1);
|
||||
el1.equals(finder.getImageElement(el1.id)).should.be.true;
|
||||
_.isUndefined(finder.getImageElement(el2.id)).should.be.true;
|
||||
});
|
||||
it('once cache reaches max size, should eject image elements', function () {
|
||||
const el1 = new ImageElement(defTemplate, defRect);
|
||||
const el2 = new ImageElement(defTemplate, defRect);
|
||||
const cache = new ImageElementFinder(null, defTemplate.length + 1).imgElCache;
|
||||
cache.set(el1.id, el1);
|
||||
cache.has(el1.id).should.be.true;
|
||||
cache.set(el2.id, el2);
|
||||
cache.has(el2.id).should.be.true;
|
||||
cache.has(el1.id).should.be.false;
|
||||
const el1 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
const el2 = new ImageElement({
|
||||
template: defTemplate,
|
||||
rect: defRect,
|
||||
});
|
||||
const finder = new ImageElementFinder(1);
|
||||
finder.registerImageElement(el1);
|
||||
_.isUndefined(finder.getImageElement(el1.id)).should.be.false;
|
||||
finder.registerImageElement(el2);
|
||||
_.isUndefined(finder.getImageElement(el1.id)).should.be.true;
|
||||
_.isUndefined(finder.getImageElement(el2.id)).should.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import {ImageElementPlugin, IMAGE_STRATEGY} from '../../lib/plugin';
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE, MATCH_TEMPLATE_MODE} from '../../lib/compare';
|
||||
import {ImageElementPlugin} from '../../lib/plugin';
|
||||
import {
|
||||
MATCH_FEATURES_MODE,
|
||||
GET_SIMILARITY_MODE,
|
||||
MATCH_TEMPLATE_MODE,
|
||||
IMAGE_STRATEGY,
|
||||
} from '../../lib/constants';
|
||||
import BaseDriver from 'appium/driver';
|
||||
import {W3C_ELEMENT_KEY} from '../../lib/finder';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures';
|
||||
import {util} from '@appium/support';
|
||||
|
||||
describe('ImageElementPlugin#handle', function () {
|
||||
const next = () => {};
|
||||
@@ -26,8 +31,8 @@ describe('ImageElementPlugin#handle', function () {
|
||||
next,
|
||||
driver,
|
||||
GET_SIMILARITY_MODE,
|
||||
TEST_IMG_1_B64,
|
||||
TEST_IMG_2_B64,
|
||||
Buffer.from(TEST_IMG_1_B64, 'base64'),
|
||||
Buffer.from(TEST_IMG_2_B64, 'base64'),
|
||||
{}
|
||||
);
|
||||
res.score.should.be.above(0.2);
|
||||
@@ -50,13 +55,26 @@ describe('ImageElementPlugin#handle', function () {
|
||||
.compareImages(next, driver, 'some mode', '', '')
|
||||
.should.eventually.be.rejectedWith(/comparison mode is unknown/);
|
||||
});
|
||||
it('should throw an error if image template is broken', async function () {
|
||||
await p.compareImages(
|
||||
next,
|
||||
driver,
|
||||
MATCH_TEMPLATE_MODE,
|
||||
Buffer.from('d1423423424'),
|
||||
Buffer.from('d1423423424')
|
||||
).should.eventually.be.rejected;
|
||||
});
|
||||
it('should throw an error if image template is empty', async function () {
|
||||
await p.compareImages(next, driver, MATCH_TEMPLATE_MODE, Buffer.from(''), Buffer.from(''))
|
||||
.should.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
describe('findElement(s)', function () {
|
||||
driver.settings = {getSettings: () => ({})};
|
||||
driver.isW3CProtocol = () => true;
|
||||
driver.getScreenshot = () => TEST_IMG_2_B64;
|
||||
driver.getWindowSize = () => ({width: 64, height: 64});
|
||||
driver.getWindowRect = () => ({x: 0, y: 0, width: 64, height: 64});
|
||||
it('should defer execution to regular command if not a find command', async function () {
|
||||
const next = () => true;
|
||||
await p.handle(next, driver, 'sendKeys').should.eventually.become(true);
|
||||
@@ -68,12 +86,12 @@ describe('ImageElementPlugin#handle', function () {
|
||||
});
|
||||
it('should find an image element inside a screenshot', async function () {
|
||||
const el = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
el[W3C_ELEMENT_KEY].should.include('appium-image-element');
|
||||
util.unwrapElement(el).should.include('appium-image-element');
|
||||
});
|
||||
it('should find image elements inside a screenshot', async function () {
|
||||
const els = await p.findElements(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
els.should.have.length(1);
|
||||
els[0][W3C_ELEMENT_KEY].should.include('appium-image-element');
|
||||
util.unwrapElement(els[0]).should.include('appium-image-element');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,9 +101,9 @@ describe('ImageElementPlugin#handle', function () {
|
||||
driver.settings = {getSettings: () => ({})};
|
||||
driver.isW3CProtocol = () => true;
|
||||
driver.getScreenshot = () => TEST_IMG_2_B64;
|
||||
driver.getWindowSize = () => ({width: 64, height: 64});
|
||||
driver.getWindowRect = () => ({x: 0, y: 0, width: 64, height: 64});
|
||||
const el = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
elId = el[W3C_ELEMENT_KEY];
|
||||
elId = util.unwrapElement(el);
|
||||
});
|
||||
it('should click on the screen coords of the middle of the element', async function () {
|
||||
let action = null;
|
||||
@@ -140,7 +158,7 @@ describe('ImageElementPlugin#handle', function () {
|
||||
}),
|
||||
};
|
||||
const el = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
elId = el[W3C_ELEMENT_KEY];
|
||||
elId = util.unwrapElement(el);
|
||||
await p
|
||||
.handle(next, driver, 'getAttribute', 'visual', elId)
|
||||
.should.eventually.include('iVBOR');
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.1.0](https://github.com/appium/appium/compare/@appium/opencv@2.0.10...@appium/opencv@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:** Make sure allocated CV objects are properly released afterwards ([#18569](https://github.com/appium/appium/issues/18569)) ([2fb844c](https://github.com/appium/appium/commit/2fb844c8ad5ea1e36c326ce5cf46869c0ff0d4f9))
|
||||
* **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/opencv@2.0.9...@appium/opencv@2.0.10) (2023-04-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **opencv:** fix a type error causing typedoc problems ([bb11d1c](https://github.com/appium/appium/commit/bb11d1cfe9681ccd457f0677118725a6e9ddd72f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.9](https://github.com/appium/appium/compare/@appium/opencv@2.0.8...@appium/opencv@2.0.9) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/opencv
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class OpenCvAutoreleasePool {
|
||||
/**
|
||||
* @type {Set}
|
||||
*/
|
||||
_items;
|
||||
|
||||
constructor() {
|
||||
this._items = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {...T} items
|
||||
* @return {T[]|T} the same items
|
||||
*/
|
||||
add(...items) {
|
||||
for (const item of items) {
|
||||
if (_.has(item, 'delete') && _.isFunction(item.delete)) {
|
||||
this._items.add(item);
|
||||
}
|
||||
}
|
||||
return items.length === 1 ? items[0] : items;
|
||||
}
|
||||
|
||||
drain() {
|
||||
for (const item of this._items) {
|
||||
try {
|
||||
item.delete();
|
||||
} catch (ign) {}
|
||||
}
|
||||
this._items.clear();
|
||||
}
|
||||
}
|
||||
+129
-136
@@ -1,7 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import Jimp from 'jimp';
|
||||
import {Buffer} from 'buffer';
|
||||
import B from 'bluebird';
|
||||
import sharp from 'sharp';
|
||||
import { OpenCvAutoreleasePool } from './autorelease-pool';
|
||||
|
||||
/** @type {any} */
|
||||
let cv;
|
||||
@@ -104,6 +105,7 @@ async function initOpenCv() {
|
||||
* for brute-force matching.
|
||||
* Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html
|
||||
* for more details.
|
||||
* The caller is responsible for releasing the returned objects.
|
||||
*
|
||||
* @param {OpenCVBindings['Mat']} img Image data
|
||||
* @param {OpenCVBindings['FeatureDetector']} detector OpenCV feature detector instance
|
||||
@@ -124,7 +126,7 @@ function detectAndCompute(img, detector) {
|
||||
/**
|
||||
* Calculated the bounding rect coordinates for the array of matching points
|
||||
*
|
||||
* @param {Array<Point>} matchedPoints Array of matching points
|
||||
* @param {Point[]} matchedPoints Array of matching points
|
||||
* @returns {Rect} The matching bounding rect or a zero rect if no match
|
||||
* can be found.
|
||||
*/
|
||||
@@ -173,28 +175,33 @@ function highlightRegion(mat, region) {
|
||||
return;
|
||||
}
|
||||
|
||||
// highlight in red
|
||||
const color = new cv.Scalar(255, 0, 0, 255);
|
||||
const thickness = 2;
|
||||
const topLeft = new cv.Point(region.x, region.y);
|
||||
const botRight = new cv.Point(region.x + region.width, region.y + region.height);
|
||||
cv.rectangle(mat, topLeft, botRight, color, thickness, cv.LINE_8, 0);
|
||||
return mat;
|
||||
const pool = new OpenCvAutoreleasePool();
|
||||
try {
|
||||
// highlight in red
|
||||
const color = pool.add(new cv.Scalar(255, 0, 0, 255));
|
||||
const thickness = 2;
|
||||
const topLeft = pool.add(new cv.Point(region.x, region.y));
|
||||
const botRight = pool.add(new cv.Point(region.x + region.width, region.y + region.height));
|
||||
cv.rectangle(mat, topLeft, botRight, color, thickness, cv.LINE_8, 0);
|
||||
return mat;
|
||||
} finally {
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef MatchingOptions
|
||||
* @property {?string} detectorName ['ORB'] One of possible OpenCV feature detector names
|
||||
* @property {string} detectorName ['ORB'] One of possible OpenCV feature detector names
|
||||
* from keys of the `AVAILABLE_DETECTORS` object.
|
||||
* Some of these methods (FAST, AGAST, GFTT, FAST, SIFT and MSER) are not available
|
||||
* in the default OpenCV installation and have to be enabled manually before
|
||||
* library compilation.
|
||||
* @property {?string} matchFunc ['BruteForce'] The name of the matching function.
|
||||
* @property {string} matchFunc ['BruteForce'] The name of the matching function.
|
||||
* Should be one of the keys of the `AVAILABLE_MATCHING_FUNCTIONS` object.
|
||||
* @property {?number|Function} goodMatchesFactor The maximum count of "good" matches
|
||||
* @property {number|Function?} goodMatchesFactor The maximum count of "good" matches
|
||||
* (e. g. with minimal distances) or a function, which accepts 3 arguments: the current distance,
|
||||
* minimal distance, maximum distance and returns true or false to include or exclude the match.
|
||||
* @property {?boolean} visualize [false] Whether to return the resulting visalization
|
||||
* @property {boolean?} visualize [false] Whether to return the resulting visalization
|
||||
* as an image (useful for debugging purposes)
|
||||
*/
|
||||
|
||||
@@ -206,13 +213,13 @@ function highlightRegion(mat, region) {
|
||||
* It is equal to `count` if `goodMatchesFactor` does not limit the matches,
|
||||
* otherwise it contains the total count of matches before `goodMatchesFactor` is
|
||||
* applied.
|
||||
* @property {?Buffer} visualization The visualization of the matching result
|
||||
* @property {Buffer?} visualization The visualization of the matching result
|
||||
* represented as PNG image buffer. This visualization looks like
|
||||
* https://user-images.githubusercontent.com/31125521/29702731-c79e3142-8972-11e7-947e-db109d415469.jpg
|
||||
* @property {Array<Point>} points1 The array of matching points on the first image
|
||||
* @property {Point[]} points1 The array of matching points on the first image
|
||||
* @property {Rect} rect1 The bounding rect for the `matchedPoints1` set or a zero rect
|
||||
* if not enough matching points are found
|
||||
* @property {Array<Point>} points2 The array of matching points on the second image
|
||||
* @property {Point[]} points2 The array of matching points on the second image
|
||||
* @property {Rect} rect2 The bounding rect for the `matchedPoints2` set or a zero rect
|
||||
* if not enough matching points are found
|
||||
*/
|
||||
@@ -223,15 +230,15 @@ function highlightRegion(mat, region) {
|
||||
*
|
||||
* @param {Buffer} img1Data The data of the first image packed into a NodeJS buffer
|
||||
* @param {Buffer} img2Data The data of the second image packed into a NodeJS buffer
|
||||
* @param {?MatchingOptions} options [{}] Set of matching options
|
||||
* @param {MatchingOptions} options [{}] Set of matching options
|
||||
*
|
||||
* @returns {MatchingResult} Maching result
|
||||
* @returns {Promise<MatchingResult>} Maching result
|
||||
* @throws {Error} If `detectorName` value is unknown.
|
||||
*/
|
||||
async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
await initOpenCv();
|
||||
|
||||
let img1, img2, detector, result1, result2, matcher, matchesVec;
|
||||
const pool = new OpenCvAutoreleasePool();
|
||||
try {
|
||||
const {
|
||||
detectorName = 'ORB',
|
||||
@@ -254,13 +261,15 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
detector = new cv[AVAILABLE_DETECTORS[detectorName]]();
|
||||
[img1, img2] = await B.all([cvMatFromImage(img1Data), cvMatFromImage(img2Data)]);
|
||||
result1 = detectAndCompute(img1, detector);
|
||||
result2 = detectAndCompute(img2, detector);
|
||||
matcher = new cv.DescriptorMatcher(AVAILABLE_MATCHING_FUNCTIONS[matchFunc]);
|
||||
matchesVec = new cv.DMatchVector();
|
||||
let matches = [];
|
||||
const detector = pool.add(new cv[AVAILABLE_DETECTORS[detectorName]]());
|
||||
const [img1, img2] = (await B.all([cvMatFromImage(img1Data), cvMatFromImage(img2Data)]))
|
||||
.map((x) => pool.add(x));
|
||||
const result1 = detectAndCompute(img1, detector);
|
||||
pool.add(result1.keyPoints, result1.descriptor);
|
||||
const result2 = detectAndCompute(img2, detector);
|
||||
pool.add(result2.keyPoints, result2.descriptor);
|
||||
const matcher = pool.add(new cv.DescriptorMatcher(AVAILABLE_MATCHING_FUNCTIONS[matchFunc]));
|
||||
const matchesVec = pool.add(new cv.DMatchVector());
|
||||
matcher.match(result1.descriptor, result2.descriptor, matchesVec);
|
||||
const totalCount = matchesVec.size();
|
||||
if (totalCount < 1) {
|
||||
@@ -269,6 +278,7 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
`resolution, or use another detector or matching function.`
|
||||
);
|
||||
}
|
||||
let matches = [];
|
||||
for (let i = 0; i < totalCount; i++) {
|
||||
matches.push(matchesVec.get(i));
|
||||
}
|
||||
@@ -313,12 +323,12 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
count: matches.length,
|
||||
};
|
||||
if (visualize) {
|
||||
const goodMatchesVec = new cv.DMatchVector();
|
||||
const goodMatchesVec = pool.add(new cv.DMatchVector());
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
goodMatchesVec.push_back(matches[i]);
|
||||
}
|
||||
const visualization = new cv.Mat();
|
||||
const color = new cv.Scalar(0, 255, 0, 255);
|
||||
const visualization = pool.add(new cv.Mat());
|
||||
const color = pool.add(new cv.Scalar(0, 255, 0, 255));
|
||||
cv.drawMatches(
|
||||
img1,
|
||||
result1.keyPoints,
|
||||
@@ -335,37 +345,27 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
width: rect2.width,
|
||||
height: rect2.height,
|
||||
});
|
||||
result.visualization = await jimpImgFromCvMat(visualization).getBufferAsync(Jimp.MIME_PNG);
|
||||
result.visualization = await cvMatToPng(visualization);
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
try {
|
||||
img1.delete();
|
||||
img2.delete();
|
||||
detector.delete();
|
||||
result1.keyPoints.delete();
|
||||
result1.descriptor.delete();
|
||||
result2.keyPoints.delete();
|
||||
result2.descriptor.delete();
|
||||
matcher.delete();
|
||||
matchesVec.delete();
|
||||
} catch (ign) {}
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef SimilarityOptions
|
||||
* @property {?boolean} visualize [false] Whether to return the resulting visalization
|
||||
* @property {boolean?} visualize [false] Whether to return the resulting visalization
|
||||
* as an image (useful for debugging purposes)
|
||||
* @property {string} method [TM_CCOEFF_NORMED] The name of the template matching method.
|
||||
* Acceptable values are:
|
||||
* - TM_CCOEFF
|
||||
* - TM_CCOEFF_NORMED (default)
|
||||
* - TM_CCORR
|
||||
* - TM_CCORR_NORMED
|
||||
* - TM_SQDIFF
|
||||
* - TM_SQDIFF_NORMED
|
||||
* - `TM_CCOEFF`
|
||||
* - `TM_CCOEFF_NORMED` (default)
|
||||
* - `TM_CCORR`
|
||||
* - `TM_CCORR_NORMED`
|
||||
* - `TM_SQDIFF`
|
||||
* - `TM_SQDIFF_NORMED`
|
||||
* Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
|
||||
* for more details.
|
||||
*/
|
||||
@@ -374,7 +374,7 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
* @typedef SimilarityResult
|
||||
* @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} visualization The visualization of the matching result
|
||||
* @property {Buffer?} visualization The visualization of the matching result
|
||||
* represented as PNG image buffer. This image includes both input pictures where
|
||||
* difference regions are highlighted with rectangles.
|
||||
*/
|
||||
@@ -385,9 +385,9 @@ async function getImagesMatches(img1Data, img2Data, options = {}) {
|
||||
*
|
||||
* @param {Buffer} img1Data The data of the first image packed into a NodeJS buffer
|
||||
* @param {Buffer} img2Data The data of the second image packed into a NodeJS buffer
|
||||
* @param {?SimilarityOptions} options [{}] Set of similarity calculation options
|
||||
* @param {SimilarityOptions} options [{}] Set of similarity calculation options
|
||||
*
|
||||
* @returns {SimilarityResult} The calculation result
|
||||
* @returns {Promise<SimilarityResult>} The calculation result
|
||||
* @throws {Error} If the given images have different resolution.
|
||||
*/
|
||||
async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
@@ -395,9 +395,10 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
|
||||
const {method = DEFAULT_MATCHING_METHOD, visualize = false} = options;
|
||||
|
||||
let template, reference, matched;
|
||||
const pool = new OpenCvAutoreleasePool();
|
||||
try {
|
||||
[template, reference] = await B.all([cvMatFromImage(img1Data), cvMatFromImage(img2Data)]);
|
||||
const [template, reference] = (await B.all([cvMatFromImage(img1Data), cvMatFromImage(img2Data)]))
|
||||
.map((x) => pool.add(x));
|
||||
if (template.rows !== reference.rows || template.cols !== reference.cols) {
|
||||
throw new Error(
|
||||
'Both images are expected to have the same size in order to ' +
|
||||
@@ -407,7 +408,7 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
template.convertTo(template, cv.CV_8UC3);
|
||||
reference.convertTo(reference, cv.CV_8UC3);
|
||||
|
||||
matched = new cv.Mat();
|
||||
const matched = pool.add(new cv.Mat());
|
||||
cv.matchTemplate(reference, template, matched, toMatchingMethod(method));
|
||||
const minMax = cv.minMaxLoc(matched);
|
||||
const result = {
|
||||
@@ -415,51 +416,36 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
};
|
||||
|
||||
if (visualize) {
|
||||
let bothImages, resultMat, mask, contours, hierarchy;
|
||||
try {
|
||||
resultMat = new cv.Mat(template.rows, template.cols * 2, cv.CV_8UC3);
|
||||
bothImages = new cv.MatVector();
|
||||
bothImages.push_back(reference);
|
||||
bothImages.push_back(template);
|
||||
cv.hconcat(bothImages, resultMat);
|
||||
const resultMat = pool.add(new cv.Mat(template.rows, template.cols * 2, cv.CV_8UC3));
|
||||
const bothImages = pool.add(new cv.MatVector());
|
||||
bothImages.push_back(reference);
|
||||
bothImages.push_back(template);
|
||||
cv.hconcat(bothImages, resultMat);
|
||||
|
||||
mask = new cv.Mat();
|
||||
cv.absdiff(reference, template, mask);
|
||||
cv.cvtColor(mask, mask, cv.COLOR_BGR2GRAY, 0);
|
||||
const mask = pool.add(new cv.Mat());
|
||||
cv.absdiff(reference, template, mask);
|
||||
cv.cvtColor(mask, mask, cv.COLOR_BGR2GRAY, 0);
|
||||
|
||||
cv.threshold(mask, mask, 128, 255, cv.THRESH_BINARY | cv.THRESH_OTSU);
|
||||
contours = new cv.MatVector();
|
||||
hierarchy = new cv.Mat();
|
||||
cv.findContours(mask, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
||||
cv.threshold(mask, mask, 128, 255, cv.THRESH_BINARY | cv.THRESH_OTSU);
|
||||
const contours = pool.add(new cv.MatVector());
|
||||
const hierarchy = pool.add(new cv.Mat());
|
||||
cv.findContours(mask, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
for (let i = 0; i < contours.size(); i++) {
|
||||
const boundingRect = cv.boundingRect(contours.get(i));
|
||||
highlightRegion(resultMat, boundingRect);
|
||||
highlightRegion(resultMat, {
|
||||
x: reference.cols + boundingRect.x,
|
||||
y: boundingRect.y,
|
||||
width: boundingRect.width,
|
||||
height: boundingRect.height,
|
||||
});
|
||||
}
|
||||
result.visualization = await jimpImgFromCvMat(resultMat).getBufferAsync(Jimp.MIME_PNG);
|
||||
} finally {
|
||||
try {
|
||||
bothImages.delete();
|
||||
resultMat.delete();
|
||||
mask.delete();
|
||||
contours.delete();
|
||||
hierarchy.delete();
|
||||
} catch (ign) {}
|
||||
for (let i = 0; i < contours.size(); i++) {
|
||||
const boundingRect = pool.add(cv.boundingRect(contours.get(i)));
|
||||
highlightRegion(resultMat, boundingRect);
|
||||
highlightRegion(resultMat, {
|
||||
x: reference.cols + boundingRect.x,
|
||||
y: boundingRect.y,
|
||||
width: boundingRect.width,
|
||||
height: boundingRect.height,
|
||||
});
|
||||
}
|
||||
result.visualization = await cvMatToPng(resultMat);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
try {
|
||||
template.delete();
|
||||
reference.delete();
|
||||
matched.delete();
|
||||
} catch (ign) {}
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +461,7 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'TM_CCOEFF'|'TM_CCOEFF_NORMED|'TM_CCORR'|'TM_CCORR_NORMED'|'TM_SQDIFF'|'TMSQDIFF_NORMED'} OccurrenceResultMethod
|
||||
* @typedef {'TM_CCOEFF'|'TM_CCOEFF_NORMED'|'TM_CCORR'|'TM_CCORR_NORMED'|'TM_SQDIFF'|'TMSQDIFF_NORMED'} OccurrenceResultMethod
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -492,12 +478,12 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
* - only when multiple option is passed
|
||||
* @property {OccurrenceResultMethod} [method='TM_CCOEFF_NORMED'] The name of the template matching method.
|
||||
* Acceptable values are:
|
||||
* - TM_CCOEFF
|
||||
* - TM_CCOEFF_NORMED (default)
|
||||
* - TM_CCORR
|
||||
* - TM_CCORR_NORMED
|
||||
* - TM_SQDIFF
|
||||
* - TM_SQDIFF_NORMED
|
||||
* - `TM_CCOEFF`
|
||||
* - `TM_CCOEFF_NORMED` (default)
|
||||
* - `TM_CCORR`
|
||||
* - `TM_CCORR_NORMED`
|
||||
* - `TM_SQDIFF`
|
||||
* - `TM_SQDIFF_NORMED`
|
||||
* Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
|
||||
* for more details.
|
||||
*/
|
||||
@@ -508,9 +494,9 @@ async function getImagesSimilarity(img1Data, img2Data, options = {}) {
|
||||
*
|
||||
* @param {Buffer} fullImgData The data of the full image packed into a NodeJS buffer
|
||||
* @param {Buffer} partialImgData The data of the partial image packed into a NodeJS buffer
|
||||
* @param {OccurrenceOptions?} [options] Set of occurrence calculation options
|
||||
* @param {OccurrenceOptions} [options] Set of occurrence calculation options
|
||||
*
|
||||
* @returns {OccurrenceResult}
|
||||
* @returns {Promise<OccurrenceResult>}
|
||||
* @throws {Error} If no occurrences of the partial image can be found in the full image
|
||||
*/
|
||||
async function getImageOccurrence(fullImgData, partialImgData, options = {}) {
|
||||
@@ -524,14 +510,11 @@ async function getImageOccurrence(fullImgData, partialImgData, options = {}) {
|
||||
method = DEFAULT_MATCHING_METHOD,
|
||||
} = options;
|
||||
|
||||
let fullImg, partialImg, matched;
|
||||
|
||||
const pool = new OpenCvAutoreleasePool();
|
||||
try {
|
||||
[fullImg, partialImg] = await B.all([
|
||||
cvMatFromImage(fullImgData),
|
||||
cvMatFromImage(partialImgData),
|
||||
]);
|
||||
matched = new cv.Mat();
|
||||
const [fullImg, partialImg] = await B.all([cvMatFromImage(fullImgData), cvMatFromImage(partialImgData)])
|
||||
.map((x) => pool.add(x));
|
||||
const matched = pool.add(new cv.Mat());
|
||||
const results = [];
|
||||
let visualization = null;
|
||||
|
||||
@@ -579,7 +562,7 @@ async function getImageOccurrence(fullImgData, partialImgData, options = {}) {
|
||||
if (_.isEmpty(results)) {
|
||||
// Below error message, `Cannot find any occurrences` is referenced in find by image
|
||||
throw new Error(
|
||||
`Match threshold: ${threshold}. Highest match value ` + `found was ${minMax.maxVal}`
|
||||
`Match threshold: ${threshold}. Highest match value found was ${minMax.maxVal}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -591,18 +574,23 @@ async function getImageOccurrence(fullImgData, partialImgData, options = {}) {
|
||||
}
|
||||
|
||||
if (visualize) {
|
||||
const fullHighlightedImage = fullImg.clone();
|
||||
const fullHighlightedImage = pool.add(fullImg.clone());
|
||||
|
||||
const visualisePromises = [];
|
||||
for (const result of results) {
|
||||
const singleHighlightedImage = fullImg.clone();
|
||||
const singleHighlightedImage = pool.add(fullImg.clone());
|
||||
|
||||
highlightRegion(singleHighlightedImage, result.rect);
|
||||
highlightRegion(fullHighlightedImage, result.rect);
|
||||
result.visualization = await jimpImgFromCvMat(singleHighlightedImage).getBufferAsync(
|
||||
Jimp.MIME_PNG
|
||||
);
|
||||
visualisePromises.push(cvMatToPng(singleHighlightedImage));
|
||||
}
|
||||
let restPngBuffers = [];
|
||||
[visualization, ...restPngBuffers] = await B.all(
|
||||
[cvMatToPng(fullHighlightedImage), ...visualisePromises]
|
||||
);
|
||||
for (const [result, pngBuffer] of _.zip(results, restPngBuffers)) {
|
||||
result.visualization = pngBuffer;
|
||||
}
|
||||
visualization = await jimpImgFromCvMat(fullHighlightedImage).getBufferAsync(Jimp.MIME_PNG);
|
||||
}
|
||||
return {
|
||||
rect: results[0].rect,
|
||||
@@ -611,46 +599,51 @@ async function getImageOccurrence(fullImgData, partialImgData, options = {}) {
|
||||
multiple: results,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
fullImg.delete();
|
||||
partialImg.delete();
|
||||
matched.delete();
|
||||
} catch (ign) {}
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an opencv image matrix into a Jimp image object
|
||||
* Convert an opencv image matrix into a PNG buffer
|
||||
*
|
||||
* @param {cv.Mat} mat the image matrix
|
||||
* @return {Jimp} the Jimp image
|
||||
* @param {cv.Mat} mat OpenCV image matrix
|
||||
* @return {Promise<Buffer>} PNG image data buffer
|
||||
*/
|
||||
function jimpImgFromCvMat(mat) {
|
||||
return new Jimp({
|
||||
width: mat.cols,
|
||||
height: mat.rows,
|
||||
data: Buffer.from(mat.data),
|
||||
});
|
||||
async function cvMatToPng(mat) {
|
||||
return await sharp(Buffer.from(mat.data), {
|
||||
raw: {
|
||||
width: mat.cols,
|
||||
height: mat.rows,
|
||||
channels: 4,
|
||||
}
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a binary image buffer and return a cv.Mat
|
||||
* Take an image buffer and return a cv.Mat
|
||||
*
|
||||
* @param {Buffer} img the image data buffer
|
||||
* @return {cv.Mat} the opencv matrix
|
||||
* @param {Buffer} img image data buffer. All image formats avilable for
|
||||
* https://www.npmjs.com/package/sharp node library are supported.
|
||||
* @return {Promise<cv.Mat>} OpenCV image matrix
|
||||
*/
|
||||
async function cvMatFromImage(img) {
|
||||
const jimpImg = await Jimp.read(img);
|
||||
return cv.matFromImageData(jimpImg.bitmap);
|
||||
const {data, info} = await sharp(img)
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({resolveWithObject: true});
|
||||
const {width, height} = info;
|
||||
return cv.matFromImageData({data, width, height});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out match results which have a matched neighbour
|
||||
*
|
||||
* @param {Array<Point>} nonZeroMatchResults matrix of image match results
|
||||
* @param {Point[]} nonZeroMatchResults matrix of image match results
|
||||
* @param {number} matchNeighbourThreshold The pixel distance within which we
|
||||
* consider an element being a neighbour of an existing match
|
||||
* @return {Array<Point>} the filtered array of matched points
|
||||
* @return {Point[]} the filtered array of matched points
|
||||
*/
|
||||
function filterNearMatches(nonZeroMatchResults, matchNeighbourThreshold) {
|
||||
return nonZeroMatchResults.reduce((acc, element) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/opencv",
|
||||
"version": "2.0.9",
|
||||
"version": "2.1.0",
|
||||
"description": "OpenCV-related helper library",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -43,9 +43,9 @@
|
||||
"dependencies": {
|
||||
"@types/bluebird": "3.5.38",
|
||||
"bluebird": "3.7.2",
|
||||
"jimp": "0.22.7",
|
||||
"lodash": "4.17.21",
|
||||
"opencv-bindings": "4.5.5",
|
||||
"sharp": "0.32.1",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.14](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.13...@appium/plugin-test-support@0.3.14) (2023-05-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.13](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.12...@appium/plugin-test-support@0.3.13) (2023-04-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.12](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.11...@appium/plugin-test-support@0.3.12) (2023-04-10)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.11](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.10...@appium/plugin-test-support@0.3.11) (2023-04-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/plugin-test-support",
|
||||
"version": "0.3.11",
|
||||
"version": "0.3.14",
|
||||
"description": "Testing utilities for Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,7 @@
|
||||
"test:unit": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.10.3",
|
||||
"@appium/types": "^0.11.1",
|
||||
"get-port": "5.1.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"source-map-support": "0.5.21",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.0](https://github.com/appium/appium/compare/@appium/strongbox@0.2.0...@appium/strongbox@0.3.0) (2023-04-20)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **strongbox:** The signature for an `ItemCtor` has changed. The second parameter is now a type extending `Strongbox` instead of a `container` string. This allows for deeper integration between a custom `Item` implementation and a custom `Strongbox` implementation. Further, a custom `container` now must be an absolute path after it's been slugified.
|
||||
|
||||
- The `Strongbox.clearAll()` method now accepts a `force` param which, if `true`, will rimraf the entire container.
|
||||
- Added two methods to the `Strongbox` class which can be overridden to accept custom options, apply defaults to those options, then validate and/or transform them.
|
||||
- Added nearly-complete unit test coverage and added E2E coverage of the `BaseItem` implementation.
|
||||
|
||||
### Features
|
||||
|
||||
* **strongbox:** allow rimraf of entire container ([93bddb4](https://github.com/appium/appium/commit/93bddb4c73ab16fffda09909786074b9e5f2b7e5))
|
||||
|
||||
|
||||
|
||||
# 0.2.0 (2023-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **strongbox:** create @appium/strongbox ([fd91234](https://github.com/appium/appium/commit/fd912346fade8f29f5b4d1458828ea677d7e9fcc))
|
||||
* **strongbox:** export BaseItem ([377e982](https://github.com/appium/appium/commit/377e9825de74d9929631fb331e6d348c9df0964e))
|
||||
@@ -0,0 +1,96 @@
|
||||
# @appium/strongbox
|
||||
|
||||
> Persistent storage for Appium extensions
|
||||
|
||||
## Summary
|
||||
|
||||
This package is intended to be used in [Appium](https://appium.io) extensions which need to persist data between Appium runs. An example of such data may be a device token or key.
|
||||
|
||||
`@appium/strongbox` provides a simple and extensible API for managing such data, while abstracting the underlying storage mechanism.
|
||||
|
||||
_Note:_ This module is not intended for storing sensitive data.
|
||||
|
||||
## Usage
|
||||
|
||||
First, create an instance of `Strongbox`:
|
||||
|
||||
```ts
|
||||
import {strongbox} from '@appium/strongbox';
|
||||
|
||||
const box = strongbox('my-pkg');
|
||||
```
|
||||
|
||||
This instance corresponds to a unique collection of data.
|
||||
|
||||
From here, create a placeholder for data (you will need to provide the type of data you intend to store):
|
||||
|
||||
```ts
|
||||
const item = await box.createItem<string>('my unique name');
|
||||
```
|
||||
|
||||
...or, if you already have the data on-hand:
|
||||
|
||||
```ts
|
||||
const item: Buffer|string = getSomeData();
|
||||
|
||||
const item = await box.createItemWithContents('my unique name', data);
|
||||
```
|
||||
|
||||
Either way, you can read its contents:
|
||||
|
||||
```ts
|
||||
// if the item doesn't exist, this result will be undefined
|
||||
const contents = await item.read();
|
||||
```
|
||||
|
||||
Or write new data to the item:
|
||||
|
||||
```ts
|
||||
await item.write('new stuff');
|
||||
```
|
||||
|
||||
The last-read contents of the `Item` will be available on the `contents` property, but the value of this property is only current as of the last `read()`:
|
||||
|
||||
```ts
|
||||
const {contents} = item;
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
In lieu of actual documentation, look at the type definitions that this package ships.
|
||||
|
||||
## Customization
|
||||
|
||||
1. Create a class that implements the `Item` interface:
|
||||
|
||||
```ts
|
||||
import {strongbox, Item} from '@appium/strongbox';
|
||||
import {Foo, getFoo} from 'somewhere/else';
|
||||
|
||||
class FooItem implements Item<Foo> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. Provide this class as the `defaultCtor` option to `strongbox()`:
|
||||
|
||||
```ts
|
||||
const box = strongbox('my-pkg', {defaultCtor: FooItem});
|
||||
```
|
||||
|
||||
3. Use like you would any other `Strongbox` instance:
|
||||
|
||||
```ts
|
||||
const foo: Foo = getFoo();
|
||||
const item = await box.createItemWithValue('my unique name', Foo);
|
||||
```
|
||||
|
||||
## Default Behavior, For the Curious
|
||||
|
||||
Out-of-the-box, a `Strongbox` instance corresponds to a directory on-disk, and each `Item` (returned by `createItem()/createItemWithContents()`) corresponds to a file within that directory.
|
||||
|
||||
The directory of the `Strongbox` instance is determined by the [env-paths](https://www.npmjs.com/package/env-paths) package, and is platform-specific.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2023 OpenJS Foundation. Licensed Apache-2.0
|
||||
@@ -0,0 +1,98 @@
|
||||
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type {Item, ItemEncoding, Strongbox, Value} from '.';
|
||||
import {slugify} from './util';
|
||||
|
||||
/**
|
||||
* Base item implementation
|
||||
*
|
||||
* @remarks This class is not intended to be instantiated directly
|
||||
* @typeParam T - Type of data stored in the `Item`
|
||||
*/
|
||||
export class BaseItem<T extends Value, U extends Strongbox = Strongbox> implements Item<T> {
|
||||
/**
|
||||
* {@inheritdoc Item.value}
|
||||
*/
|
||||
protected _value?: T | undefined;
|
||||
|
||||
/**
|
||||
* Parent Strongbox instance
|
||||
*/
|
||||
public readonly container: string;
|
||||
/**
|
||||
* Unique slugified identifier
|
||||
*/
|
||||
public readonly id: string;
|
||||
/**
|
||||
* {@inheritdoc Item.value}
|
||||
*/
|
||||
public readonly value: T | undefined;
|
||||
|
||||
/**
|
||||
* Slugifies the name
|
||||
* @param name Name of instance
|
||||
* @param parent Parent Strongbox
|
||||
* @param encoding Defaults to `utf8`
|
||||
*/
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
parent: U,
|
||||
public readonly encoding: ItemEncoding = 'utf8'
|
||||
) {
|
||||
this.container = parent.container;
|
||||
this.id = path.join(this.container, slugify(name));
|
||||
Object.defineProperties(this, {
|
||||
value: {
|
||||
get() {
|
||||
return this._value;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
_value: {
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.clear}
|
||||
*/
|
||||
public async clear(): Promise<void> {
|
||||
try {
|
||||
await unlink(this.id);
|
||||
this._value = undefined;
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.read}
|
||||
*/
|
||||
public async read(): Promise<T | undefined> {
|
||||
try {
|
||||
this._value = (await readFile(this.id, {
|
||||
encoding: this.encoding,
|
||||
})) as T;
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Item.write}
|
||||
*/
|
||||
public async write(value: T): Promise<void> {
|
||||
if (this._value !== value) {
|
||||
await mkdir(path.dirname(this.id), {recursive: true});
|
||||
await writeFile(this.id, value, this.encoding);
|
||||
this._value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import envPaths from 'env-paths';
|
||||
import {rm} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {BaseItem} from './base-item';
|
||||
import {slugify} from './util';
|
||||
|
||||
/**
|
||||
* Valid file encodings.
|
||||
*
|
||||
* `null` means the file should be read and written as a `Buffer`.
|
||||
*/
|
||||
export type ItemEncoding = BufferEncoding | null;
|
||||
|
||||
/**
|
||||
* Valid value wrapped by {@linkcode Item}. Can be an encoded string or `Buffer`
|
||||
*/
|
||||
export type Value = string | Buffer;
|
||||
|
||||
/**
|
||||
* An object representing a persisted item containing something of type `T` (which can be a {@linkcode Buffer} or an encoded string; see {@linkcode ItemEncoding}).
|
||||
*
|
||||
* A {@linkcode Item} does not know anything about where it is stored, or how it is stored.
|
||||
* @typeParam T - Type of data stored in the item
|
||||
*/
|
||||
export interface Item<T extends Value> {
|
||||
/**
|
||||
* Encoding of underlying value
|
||||
*/
|
||||
encoding: ItemEncoding;
|
||||
/**
|
||||
* Slugified name
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name of item
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Last known value (stored in memory)
|
||||
*
|
||||
* @remarks A custom {@linkcode Item} meant to handle very large files should probably not implement this.
|
||||
*/
|
||||
value?: T | undefined;
|
||||
|
||||
/**
|
||||
* Deletes the item.
|
||||
*/
|
||||
clear(): Promise<void>;
|
||||
/**
|
||||
* Reads value
|
||||
*/
|
||||
read(): Promise<T | undefined>;
|
||||
/**
|
||||
* Writes value
|
||||
* @param value New value
|
||||
*/
|
||||
write(value: T): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of known `Item` encodings
|
||||
* @internal
|
||||
*/
|
||||
const ITEM_ENCODINGS: Readonly<Set<ItemEncoding>> = new Set([
|
||||
'ascii',
|
||||
'utf8',
|
||||
'utf-8',
|
||||
'utf16le',
|
||||
'ucs2',
|
||||
'ucs-2',
|
||||
'base64',
|
||||
'base64url',
|
||||
'latin1',
|
||||
'binary',
|
||||
'hex',
|
||||
null,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Type guard for encodings
|
||||
* @param value any
|
||||
* @returns `true` is `value` is a valid file encoding
|
||||
*/
|
||||
function isEncoding(value: any): value is ItemEncoding {
|
||||
return ITEM_ENCODINGS.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default container suffix if no explicit container is provided.
|
||||
*
|
||||
* @see {@linkcode StrongboxOpts}
|
||||
*/
|
||||
export const DEFAULT_SUFFIX = 'strongbox';
|
||||
|
||||
/**
|
||||
* A constructor function which instantiates a {@linkcode Item}.
|
||||
*/
|
||||
export type ItemCtor<
|
||||
T extends Value,
|
||||
U extends StrongboxOpts = StrongboxOpts,
|
||||
V extends Strongbox<U> = Strongbox<U>
|
||||
> = new (name: string, parent: V, encoding?: ItemEncoding) => Item<T>;
|
||||
|
||||
/**
|
||||
* Main entry point for use of this module
|
||||
*
|
||||
* Manages multiple {@linkcode Item}s.
|
||||
*/
|
||||
export class Strongbox<Options extends StrongboxOpts = StrongboxOpts> {
|
||||
/**
|
||||
* Default {@linkcode ItemCtor} to use when creating new {@linkcode Item}s
|
||||
*/
|
||||
protected defaultItemCtor: ItemCtor<any>;
|
||||
/**
|
||||
* Store of known {@linkcode Item}s
|
||||
* @internal
|
||||
*/
|
||||
protected items: Map<string, WeakRef<Item<any>>>;
|
||||
|
||||
/**
|
||||
* Override the directory of this container.
|
||||
*
|
||||
* If this is present, `suffix` is ignored.
|
||||
*/
|
||||
public readonly container: string;
|
||||
/**
|
||||
* Slugified name of this instance; corresponds to the directory name.
|
||||
*
|
||||
* If `dir` is provided, this value is unused.
|
||||
* If `suffix` is provided, then this will be the parent directory of `suffix`.
|
||||
*/
|
||||
public readonly id: string;
|
||||
public readonly suffix: string;
|
||||
|
||||
/**
|
||||
* Slugifies the name & determines the directory
|
||||
* @param name Name of instance
|
||||
* @param opts Options
|
||||
*/
|
||||
protected constructor(public readonly name: string, opts: Partial<Options> = {}) {
|
||||
this.id = slugify(name);
|
||||
|
||||
let newOpts = this.setDefaultOptions(opts);
|
||||
newOpts = this.checkOptions(newOpts);
|
||||
|
||||
this.defaultItemCtor = newOpts.defaultItemCtor;
|
||||
this.container = newOpts.container;
|
||||
this.suffix = newOpts.suffix;
|
||||
this.items = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@linkcode Strongbox}
|
||||
* @param name Name of instance
|
||||
* @param opts Options
|
||||
* @returns New instance
|
||||
*/
|
||||
public static create<Options extends StrongboxOpts = StrongboxOpts>(
|
||||
name: string,
|
||||
opts?: Partial<Options>
|
||||
) {
|
||||
return new Strongbox(name, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears _all_ items.
|
||||
*
|
||||
* @param force - If `true`, will rimraf the container. Otherwise, will only delete individual items.
|
||||
*/
|
||||
public async clearAll(force = false): Promise<void> {
|
||||
const items = [...this.items.values()].map((ref) => ref.deref()).filter(Boolean) as Item<any>[];
|
||||
await Promise.all(items.map((item) => item.clear()));
|
||||
if (force) {
|
||||
await rm(this.container, {recursive: true});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@linkcode Item}.
|
||||
*
|
||||
* Reads the item, if it is already persisted. Does not throw if missing.
|
||||
* @param name Unique name of item
|
||||
* @param encoding Encoding of item; defaults to `utf8`
|
||||
* @returns New `Item`
|
||||
* @typeParam T - Type of data stored in the `Item`
|
||||
*/
|
||||
async createItem<T extends Value>(
|
||||
name: string,
|
||||
ctor?: ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItem<T extends Value>(name: string, encoding?: ItemEncoding): Promise<Item<T>>;
|
||||
async createItem<T extends Value>(
|
||||
name: string,
|
||||
encodingOrCtor?: ItemEncoding | ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>> {
|
||||
if (isEncoding(encodingOrCtor)) {
|
||||
encoding = encodingOrCtor;
|
||||
encodingOrCtor = this.defaultItemCtor;
|
||||
}
|
||||
const item = new (encodingOrCtor ?? (this.defaultItemCtor as ItemCtor<T>))(
|
||||
name,
|
||||
this,
|
||||
encoding
|
||||
);
|
||||
if (this.items.has(item.id)) {
|
||||
throw new ReferenceError(`Item with id "${item.id}" already exists`);
|
||||
}
|
||||
try {
|
||||
await item.read();
|
||||
} catch (e) {
|
||||
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.items.set(item.id, new WeakRef(item));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@linkcode Item} then immediately writes value to it.
|
||||
*
|
||||
* If it exists already, it will be overwritten.
|
||||
* @param name Name of `Item`
|
||||
* @param value File value to write
|
||||
* @param ctor Specific {@linkcode ItemCtor} to use
|
||||
* @param encoding File encoding
|
||||
* @returns New `Item` w/ value of `value`
|
||||
*/
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
ctor: ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>>;
|
||||
async createItemWithValue<T extends Value>(
|
||||
name: string,
|
||||
value: T,
|
||||
encodingOrCtor?: ItemEncoding | ItemCtor<T>,
|
||||
encoding?: ItemEncoding
|
||||
): Promise<Item<T>> {
|
||||
const item = isEncoding(encodingOrCtor)
|
||||
? await this.createItem<T>(name, encodingOrCtor)
|
||||
: await this.createItem<T>(name, encodingOrCtor, encoding);
|
||||
await item.write(value);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve an {@linkcode Item} by its `id`.
|
||||
* @param id ID of item
|
||||
* @returns An `Item`, if found
|
||||
*/
|
||||
public getItem(id: string): Item<any> | undefined {
|
||||
const ref = this.items.get(id);
|
||||
return ref?.deref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs runtime validation (and optionally transformation) of options.
|
||||
*
|
||||
* Should not set defaults.
|
||||
*
|
||||
* The default implementation slugifies any custom container name and suffix.
|
||||
*
|
||||
* Subclasses should override this method to perform additional validation as needed.
|
||||
* @param opts - Options
|
||||
*/
|
||||
protected checkOptions(opts: Options): Options {
|
||||
opts.suffix = slugify(opts.suffix);
|
||||
if (opts.container) {
|
||||
opts.container = opts.container.split(path.sep).map(slugify).join(path.sep);
|
||||
if (!path.isAbsolute(opts.container)) {
|
||||
throw new TypeError(`container slug ${opts.container} must be an absolute path`);
|
||||
}
|
||||
} else {
|
||||
opts.container = path.join(envPaths(this.id).data, opts.suffix);
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets defaults for options.
|
||||
*
|
||||
* Subclasses should override as necessary.
|
||||
* @param opts Options
|
||||
* @returns Options with defaults applied
|
||||
*/
|
||||
protected setDefaultOptions(opts: Partial<Options> = {}): Options {
|
||||
const newOpts = opts as Options;
|
||||
newOpts.suffix = opts.suffix ?? DEFAULT_SUFFIX;
|
||||
newOpts.defaultItemCtor = opts.defaultItemCtor ?? BaseItem;
|
||||
return newOpts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@linkcode strongbox}
|
||||
*/
|
||||
export interface StrongboxOpts {
|
||||
/**
|
||||
* Override default container, which is chosen according to environment.
|
||||
*
|
||||
* This must be a writable path.
|
||||
*/
|
||||
container: string;
|
||||
/**
|
||||
* Default {@linkcode Item} constructor.
|
||||
*
|
||||
* Unless a constructor is specified when calling {@linkcode Strongbox.createItem} or {@linkcode Strongbox.createItemWithValue}, this will be used.
|
||||
* @defaultValue BaseItem
|
||||
*/
|
||||
defaultItemCtor: ItemCtor<any>;
|
||||
/**
|
||||
* Extra subdir to append to the auto-generated file directory hierarchy.
|
||||
*
|
||||
* This is ignored if `container` is provided.
|
||||
* @defaultValue 'strongbox'
|
||||
*/
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Strongbox.create}
|
||||
*/
|
||||
export const strongbox = Strongbox.create;
|
||||
|
||||
/**
|
||||
* This can be subclassed if needed.
|
||||
*/
|
||||
export {BaseItem};
|
||||
@@ -0,0 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import slug from 'slugify';
|
||||
|
||||
export function slugify(value: string) {
|
||||
return slug(value, {lower: true});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@appium/strongbox",
|
||||
"version": "0.3.0",
|
||||
"description": "Persistent storage for Appium extensions",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/strongbox"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"main": "./build/lib/index.js",
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"build/lib",
|
||||
"lib",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "run-p test:unit test:types",
|
||||
"test:smoke": "node .",
|
||||
"test:unit": "mocha \"test/unit/**/*.spec.ts\"",
|
||||
"test:types": "tsd",
|
||||
"test:e2e": "mocha -t 20s \"test/e2e/**/*.e2e.spec.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"env-paths": "2.2.1",
|
||||
"slugify": "1.6.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"tsd": {
|
||||
"directory": "test/types"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {Item, Strongbox, strongbox} from '../../lib';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('@appium/strongbox', function () {
|
||||
describe('default behavior', function () {
|
||||
let box: Strongbox;
|
||||
|
||||
beforeEach(function () {
|
||||
box = strongbox('test');
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await box.clearAll(true);
|
||||
});
|
||||
|
||||
describe('when creating an Item with a value', function () {
|
||||
let item: Item<string>;
|
||||
|
||||
beforeEach(async function () {
|
||||
item = await box.createItemWithValue('test', 'value');
|
||||
});
|
||||
|
||||
it('should write the value to the filesystem', async function () {
|
||||
await expect(readFile(item.id, 'utf8')).to.eventually.equal('value');
|
||||
});
|
||||
|
||||
it('should set the value property', async function () {
|
||||
expect(item.value).to.equal('value');
|
||||
});
|
||||
|
||||
describe('when writing a new value', function () {
|
||||
beforeEach(async function () {
|
||||
await item.write('new value');
|
||||
});
|
||||
|
||||
it('should write the value to the filesystem', async function () {
|
||||
await expect(readFile(item.id, 'utf8')).to.eventually.equal('new value');
|
||||
});
|
||||
|
||||
it('should set the value property', async function () {
|
||||
expect(item.value).to.equal('new value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clearing the item', function () {
|
||||
beforeEach(async function () {
|
||||
await item.clear();
|
||||
});
|
||||
|
||||
it('should remove the item from the filesystem', async function () {
|
||||
await expect(readFile(item.id, 'utf8')).to.be.rejectedWith('ENOENT');
|
||||
});
|
||||
|
||||
it('should set the value property to undefined', async function () {
|
||||
expect(item.value).to.be.undefined;
|
||||
});
|
||||
|
||||
describe('when attempting to read it again', function () {
|
||||
it('should resolve w/ undefined', async function () {
|
||||
await expect(item.read()).to.eventually.be.undefined;
|
||||
});
|
||||
|
||||
it('should set the value property to undefined', async function () {
|
||||
expect(item.value).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user