diff --git a/.build/pre-release.sh b/.build/pre-release.sh
index fe44496a..1f515ee3 100644
--- a/.build/pre-release.sh
+++ b/.build/pre-release.sh
@@ -1,13 +1,8 @@
#!/usr/bin/env bash
set -ex
-echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
-npm whoami
-
-npm ci
patchVersion=$(npm --no-git-tag version patch)
nextVersion=${patchVersion}-next."$(date +%Y%m%d%H%M%S)"
echo "${nextVersion:1}"
-npm version --no-git-tag -f "${nextVersion:1}"
-npm run publish-next
\ No newline at end of file
+npm version --no-git-tag -f "${nextVersion:1}"
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index eef4ab05..fe1d11cf 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -43,7 +43,7 @@ jobs:
npm run coverage:merge
npm run coverage:merge-report
- name: Send results to SonarCloud
- uses: SonarSource/sonarcloud-github-action@v1.4
+ uses: SonarSource/sonarcloud-github-action@v1.6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -53,11 +53,8 @@ jobs:
- sonar
strategy:
matrix:
- os: [ ubuntu-latest, windows-latest, macos-latest ]
- node: [ 12, 14, 16 ]
- exclude:
- - os: ubuntu-latest
- node: 14
+ os: [ windows-latest, macos-latest ]
+ node: [ 14 ]
runs-on: ${{matrix.os}}
steps:
- name: Set up Git repository
diff --git a/.github/workflows/snapshot_release.yaml b/.github/workflows/snapshot_release.yaml
index e7dd13dc..816061cc 100644
--- a/.github/workflows/snapshot_release.yaml
+++ b/.github/workflows/snapshot_release.yaml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
- node: [ 12, 14, 16 ]
+ node: [ 14 ]
runs-on: ${{matrix.os}}
steps:
- name: Set up Git repository
@@ -58,9 +58,21 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 14
+ registry-url: 'https://registry.npmjs.org'
+ - name: Install
+ run: npm ci
- name: Install @nut-tree/libnut@next
run: npm i @nut-tree/libnut@next
- - name: Publish snapshot release
+ - name: Create snapshot release
run: bash ./.build/pre-release.sh
+ - name: Publish snapshot release to npm
+ run: npm run publish-next
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - uses: actions/setup-node@v2
+ with:
+ registry-url: 'https://npm.pkg.github.com'
+ - name: Publish snapshot release to GPR
+ run: npm run publish-next
env:
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/tagged_release.yaml b/.github/workflows/tagged_release.yaml
index 2dfc280f..fc32a9d9 100644
--- a/.github/workflows/tagged_release.yaml
+++ b/.github/workflows/tagged_release.yaml
@@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
- node: [ 12, 14, 16 ]
+ node: [ 14 ]
runs-on: ${{matrix.os}}
steps:
- name: Set up Git repository
@@ -52,6 +52,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 14
+ registry-url: 'https://registry.npmjs.org'
- name: Install
run: npm ci
- name: Run typedoc
@@ -62,7 +63,14 @@ jobs:
deploy_key: ${{ secrets.API_DOC_DEPLOY_KEY }}
external_repository: nut-tree/apidoc
publish_dir: ./docs
- - name: Publish tagged release
- uses: JS-DevTools/npm-publish@v1
+ - name: Publish tagged release to npm
+ run: npm publish
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - uses: actions/setup-node@v2
with:
- token: ${{ secrets.NPM_TOKEN }}
+ registry-url: 'https://npm.pkg.github.com'
+ - name: Publish tagged release to GPR
+ run: npm publish
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5bc2932d..7b5430b7 100644
--- a/README.md
+++ b/README.md
@@ -5,23 +5,15 @@
|Master ||
|Develop||
-
-
-
-
[](https://sonarcloud.io/dashboard?id=nut-tree%3Anut.js)
[](https://sonarcloud.io/component_measures?id=nut-tree%3Anut.js&metric=coverage)
[](https://www.npmjs.com/package/@nut-tree/nut-js)
-
+
Native UI testing / automation with node.js
-
-
-
-
# About
@@ -34,10 +26,30 @@ Native UI testing / automation with node.js
It allows for native UI interactions via keyboard and / or mouse,
but additionally gives you the possibility to navigate the screen based on image matching.
+# Sponsoring
+
+`nut.js` is developed with community in mind.
+
+A huge **"Thank you!"** goes out to all sponsors who make open source a bit more sustainable!
+
+[
](https://github.com/Reiss-Cashmore)
+[
](https://github.com/ccorcos)
+[
](https://github.com/stoefln)
+
+# Demo
+
+Check out this demo video to get a first impression of what nut.js is capable of.
+
+[](https://www.youtube.com/watch?v=MpIyUJnU_Bk)
+
# Examples
[nut-tree/trailmix](https://github.com/nut-tree/trailmix) contains a set of ready to use examples which demo the usage ot nut.js.
+# API Docs
+
+nut.js provides [public API documentation](https://nut-tree.github.io/apidoc/) auto-generated by [TypeDoc](https://typedoc.org).
+
# Discussion
In [nut-tree/rfc](https://github.com/nut-tree/rfc) documents regarding larger design / implementation changes in nut.js are up for discussion.
@@ -59,7 +71,7 @@ It's work in progress and will undergo constant modification.
## Keyboard
- [x] Support for standard US keyboard layout
-- [x] Support for German special characters
+- [x] Support for multimedia keys
## Mouse
@@ -77,8 +89,10 @@ It's work in progress and will undergo constant modification.
## Screen
-- [x] findOnScreen
-- [x] waitFor
+- [x] Find an image on screen
+- [x] Find all image occurrences on screen
+- [x] Wait for an image to appear on screen
+- [x] Retrieve RGBA color information on screen
- [x] Hooks to trigger actions based on images
- [x] Highlighting screen regions
@@ -201,96 +215,3 @@ will install the most recent development release of `nut.js`.
**Attention**: While snapshot releases are great to work with upcoming features before a new stable release, it is still a snapshot release.
Please bear in mind that things might change and / or break on snapshot releases, so it is not recommended using them in production.
-
-### Usage with Electron
-
-`nut.js` in combination with Electron requires bindings built for use with Electron.
-`nut.js` does provide such bindings and e.g. [electron-rebuild](https://www.npmjs.com/package/electron-rebuild) makes installation a breeze.
-
-Besides installing `nut.js` via
-
-```bash
-npm i @nut-tree/nut-js
-```
-
-or
-
-```bash
-yarn add @nut-tree/nut-js
-```
-
-we also install `electron-rebuild` as a `devDependency`:
-
-```bash
-npm i -D electron-rebuild
-```
-
-or
-
-```bash
-yarn add -D electron-rebuild
-```
-
-Next, we add a `rebuild` script to our `package.json`:
-
-```json
-{
- ...
- "scripts": {
- ...
- "start": "electron app.js",
- "rebuild": "electron-rebuild"
- },
- ...,
-}
-```
-
-Now all we have to do is run `npm run rebuild` and `electron-rebuild` will fetch the appropriate bindings for our Electron version.
-Currently `nut.js` provides bindings for all ABI version to work with Electron v4.x up to 8.x
-
-### Manual build
-
-As a fallback, `nut.js` is able to build all required dependencies by itself.
-To do so, some setup is required on the respective target platform.
-
-#### Windows
-
-In order to install `nut.js` on Windows, [Windows Build Tools](https://www.microsoft.com/en-us/download/details.aspx?id=48159) and [Python 2](https://www.python.org/downloads/windows/) are required.
-You can either set them up manually, or install them via npm:
-
-```bash
-npm install --global windows-build-tools
-```
-
-or
-
-```bash
-yarn global add windows-build-tools
-```
-
-#### macOS
-
-On macOS, Xcode command line tools are required.
-You can install them by running
-```bash
-xcode-select --install
-```
-
-#### Linux
-
-Depending on your distribution, Linux setups may differ.
-
-In general, `nut.js` requires
-
-- Python 2
-- g++
-- make
-- libXtst
-- libPng
-
-Installation on `*buntu` distributions:
-```bash
-sudo apt-get install build-essential python libxtst-dev libpng++-dev
-```
-
-Setups on other distributions might differ.
diff --git a/e2e/assets/checkers.png b/e2e/assets/checkers.png
new file mode 100644
index 00000000..07cde4f0
Binary files /dev/null and b/e2e/assets/checkers.png differ
diff --git a/e2e/electron-test/package-lock.json b/e2e/electron-test/package-lock.json
index abc08134..52c29e6e 100644
--- a/e2e/electron-test/package-lock.json
+++ b/e2e/electron-test/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "electron-test",
- "version": "1.6.0",
+ "version": "1.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -35,9 +35,8 @@
"dev": true,
"requires": {
"@nut-tree/libnut": "2.1.2",
- "clipboardy": "2.0.0",
- "node-abort-controller": "2.0.0",
- "opencv4nodejs-prebuilt": "5.3.3"
+ "clipboardy": "2.3.0",
+ "node-abort-controller": "2.0.0"
},
"dependencies": {
"@babel/code-frame": {
@@ -845,7 +844,6 @@
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz",
"integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==",
- "dev": true,
"optional": true
},
"@types/normalize-package-data": {
@@ -975,8 +973,7 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "dev": true
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"arch": {
"version": "2.1.1",
@@ -1241,8 +1238,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
@@ -1281,7 +1277,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -1291,14 +1286,12 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -1308,14 +1301,12 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
@@ -1385,7 +1376,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -1500,8 +1490,7 @@
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"ci-info": {
"version": "2.0.0",
@@ -1742,8 +1731,7 @@
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"convert-source-map": {
"version": "1.7.0",
@@ -1766,7 +1754,7 @@
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -1835,7 +1823,6 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
- "dev": true,
"requires": {
"mimic-response": "^2.0.0"
}
@@ -1843,8 +1830,7 @@
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "dev": true
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"deep-is": {
"version": "0.1.3",
@@ -1909,8 +1895,7 @@
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "dev": true
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"detect-indent": {
"version": "6.0.0",
@@ -1920,8 +1905,7 @@
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
- "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
- "dev": true
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"detect-newline": {
"version": "3.1.0",
@@ -2064,7 +2048,7 @@
"execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
- "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
@@ -2128,8 +2112,7 @@
"expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "dev": true
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
},
"expect": {
"version": "25.4.0",
@@ -2441,8 +2424,7 @@
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "dev": true
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "5.0.0",
@@ -2529,7 +2511,7 @@
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"requires": {
"pump": "^3.0.0"
},
@@ -2561,8 +2543,7 @@
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=",
- "dev": true
+ "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
},
"glob": {
"version": "7.1.3",
@@ -2648,8 +2629,7 @@
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
- "dev": true
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"has-value": {
"version": "1.0.0",
@@ -2768,8 +2748,7 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "5.1.4",
@@ -2812,8 +2791,7 @@
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"inquirer": {
"version": "7.1.0",
@@ -4804,8 +4782,7 @@
"mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
- "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
- "dev": true
+ "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
},
"minimatch": {
"version": "3.0.4",
@@ -4869,8 +4846,7 @@
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "dev": true
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"ms": {
"version": "2.1.2",
@@ -4885,8 +4861,7 @@
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
- "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
- "dev": true
+ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"nanomatch": {
"version": "1.2.13",
@@ -4909,14 +4884,12 @@
"napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
- "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
- "dev": true
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
},
"native-node-utils": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/native-node-utils/-/native-node-utils-0.2.7.tgz",
"integrity": "sha512-61v0G3uVxWlXHppSZGwZi+ZEIgGUKI8QvEkEJLb1GVePI7P8SBe+G747z+QMXSt4TxfgbVZP0DyobbRKYVIjdw==",
- "dev": true,
"requires": {
"nan": "^2.13.2"
}
@@ -4934,13 +4907,12 @@
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y="
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node-abi": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz",
"integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==",
- "dev": true,
"requires": {
"semver": "^5.4.1"
}
@@ -5098,8 +5070,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@@ -5165,7 +5136,6 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/opencv4nodejs-prebuilt/-/opencv4nodejs-prebuilt-5.3.3.tgz",
"integrity": "sha512-c9n4bLoamHLwafqCR6W4Xh5O/35IJ2A01pWbW/4SVgYJDAhGAW/FrZw0q4VwUPxEe1cXZgr+Wy5HZUQIotkZdg==",
- "dev": true,
"requires": {
"@types/node": ">6",
"nan": "^2.14.2",
@@ -5178,7 +5148,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "dev": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@@ -5188,7 +5157,6 @@
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "dev": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
@@ -5204,7 +5172,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "dev": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@@ -5216,7 +5183,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -5231,7 +5197,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -5418,7 +5383,6 @@
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz",
"integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==",
- "dev": true,
"requires": {
"detect-libc": "^1.0.3",
"expand-template": "^2.0.3",
@@ -5439,7 +5403,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "dev": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@@ -5449,7 +5412,6 @@
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "dev": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
@@ -5465,7 +5427,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "dev": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@@ -5477,7 +5438,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -5492,7 +5452,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -5525,8 +5484,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"process-on-spawn": {
"version": "1.0.0",
@@ -5569,7 +5527,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dev": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@@ -6073,14 +6030,12 @@
"simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "dev": true
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
},
"simple-get": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
- "dev": true,
"requires": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
@@ -6434,8 +6389,7 @@
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
- "dev": true
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"supports-color": {
"version": "7.1.0",
@@ -6460,25 +6414,32 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"tar": {
- "version": "4.4.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
- "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
+ "version": "4.4.19",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
+ "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
"dev": true,
"requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.8.6",
- "minizlib": "^1.2.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.3"
+ "chownr": "^1.1.4",
+ "fs-minipass": "^1.2.7",
+ "minipass": "^2.9.0",
+ "minizlib": "^1.3.3",
+ "mkdirp": "^0.5.5",
+ "safe-buffer": "^5.2.1",
+ "yallist": "^3.1.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ }
}
},
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
- "dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@@ -6490,7 +6451,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@@ -6503,7 +6463,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -6513,14 +6472,12 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
@@ -6975,8 +6932,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.3.2",
@@ -7120,7 +7076,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
- "dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
@@ -8733,6 +8688,12 @@
"tar": "^4"
},
"dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true
+ },
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -8740,18 +8701,18 @@
"dev": true
},
"tar": {
- "version": "4.4.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
- "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
+ "version": "4.4.19",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
+ "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
"dev": true,
"requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.8.6",
- "minizlib": "^1.2.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.3"
+ "chownr": "^1.1.4",
+ "fs-minipass": "^1.2.7",
+ "minipass": "^2.9.0",
+ "minizlib": "^1.3.3",
+ "mkdirp": "^0.5.5",
+ "safe-buffer": "^5.2.1",
+ "yallist": "^3.1.1"
}
},
"yallist": {
@@ -9132,12 +9093,6 @@
"sprintf-js": "^1.1.2"
}
},
- "run-script-os": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz",
- "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==",
- "dev": true
- },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -9284,9 +9239,9 @@
}
},
"tar": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
- "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+ "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"dev": true,
"requires": {
"chownr": "^2.0.0",
diff --git a/e2e/electron-test/package.json b/e2e/electron-test/package.json
index a09cfd92..e5fb52c4 100644
--- a/e2e/electron-test/package.json
+++ b/e2e/electron-test/package.json
@@ -1,6 +1,6 @@
{
"name": "electron-test",
- "version": "1.6.0",
+ "version": "1.7.0",
"description": "A test to verify usage with Electron",
"main": "main.js",
"author": {
diff --git a/e2e/window-test/package-lock.json b/e2e/window-test/package-lock.json
index fcbb1c5b..dc8817c1 100644
--- a/e2e/window-test/package-lock.json
+++ b/e2e/window-test/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "window-integration-tests",
- "version": "1.6.0",
+ "version": "1.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -680,9 +680,8 @@
"dev": true,
"requires": {
"@nut-tree/libnut": "2.1.2",
- "clipboardy": "2.0.0",
- "node-abort-controller": "2.0.0",
- "opencv4nodejs-prebuilt": "5.3.3"
+ "clipboardy": "2.3.0",
+ "node-abort-controller": "2.0.0"
},
"dependencies": {
"@babel/code-frame": {
@@ -1490,7 +1489,6 @@
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz",
"integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==",
- "dev": true,
"optional": true
},
"@types/normalize-package-data": {
@@ -1620,13 +1618,12 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "dev": true
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"arch": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz",
- "integrity": "sha1-j1wnMao1owkpIhuwZA7tZRdeyE4="
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+ "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="
},
"archy": {
"version": "1.0.0",
@@ -1886,8 +1883,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
@@ -1926,7 +1922,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -1936,14 +1931,12 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -1953,14 +1946,12 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
@@ -2030,7 +2021,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -2145,8 +2135,7 @@
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "dev": true
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"ci-info": {
"version": "2.0.0",
@@ -2193,12 +2182,13 @@
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
},
"clipboardy": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.0.0.tgz",
- "integrity": "sha1-P87kIf3spOamLOcrZvPrDEIWWs0=",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
+ "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==",
"requires": {
"arch": "^2.1.1",
- "execa": "^1.0.0"
+ "execa": "^1.0.0",
+ "is-wsl": "^2.1.1"
}
},
"cliui": {
@@ -2387,8 +2377,7 @@
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"convert-source-map": {
"version": "1.7.0",
@@ -2411,7 +2400,7 @@
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -2480,7 +2469,6 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
- "dev": true,
"requires": {
"mimic-response": "^2.0.0"
}
@@ -2488,8 +2476,7 @@
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "dev": true
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"deep-is": {
"version": "0.1.3",
@@ -2554,8 +2541,7 @@
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "dev": true
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"detect-indent": {
"version": "6.0.0",
@@ -2565,8 +2551,7 @@
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
- "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
- "dev": true
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"detect-newline": {
"version": "3.1.0",
@@ -2649,9 +2634,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"end-of-stream": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
- "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=",
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"requires": {
"once": "^1.4.0"
}
@@ -2709,7 +2694,7 @@
"execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
- "integrity": "sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
@@ -2773,8 +2758,7 @@
"expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "dev": true
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
},
"expect": {
"version": "25.4.0",
@@ -3086,8 +3070,7 @@
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "dev": true
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "5.0.0",
@@ -3174,20 +3157,9 @@
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha1-wbJVV189wh1Zv8ec09K0axw6VLU=",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"requires": {
"pump": "^3.0.0"
- },
- "dependencies": {
- "pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- }
}
},
"get-value": {
@@ -3206,8 +3178,7 @@
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=",
- "dev": true
+ "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
},
"glob": {
"version": "7.1.3",
@@ -3293,8 +3264,7 @@
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
- "dev": true
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"has-value": {
"version": "1.0.0",
@@ -3413,8 +3383,7 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "5.1.4",
@@ -3457,8 +3426,7 @@
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"inquirer": {
"version": "7.1.0",
@@ -3674,8 +3642,7 @@
"is-wsl": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz",
- "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==",
- "optional": true
+ "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog=="
},
"isarray": {
"version": "1.0.0",
@@ -5449,8 +5416,7 @@
"mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
- "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
- "dev": true
+ "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
},
"minimatch": {
"version": "3.0.4",
@@ -5514,8 +5480,7 @@
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "dev": true
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"ms": {
"version": "2.1.2",
@@ -5530,8 +5495,7 @@
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
- "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
- "dev": true
+ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"nanomatch": {
"version": "1.2.13",
@@ -5554,14 +5518,12 @@
"napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
- "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
- "dev": true
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
},
"native-node-utils": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/native-node-utils/-/native-node-utils-0.2.7.tgz",
"integrity": "sha512-61v0G3uVxWlXHppSZGwZi+ZEIgGUKI8QvEkEJLb1GVePI7P8SBe+G747z+QMXSt4TxfgbVZP0DyobbRKYVIjdw==",
- "dev": true,
"requires": {
"nan": "^2.13.2"
}
@@ -5579,13 +5541,12 @@
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y="
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node-abi": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz",
"integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==",
- "dev": true,
"requires": {
"semver": "^5.4.1"
}
@@ -5743,8 +5704,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@@ -5810,7 +5770,6 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/opencv4nodejs-prebuilt/-/opencv4nodejs-prebuilt-5.3.3.tgz",
"integrity": "sha512-c9n4bLoamHLwafqCR6W4Xh5O/35IJ2A01pWbW/4SVgYJDAhGAW/FrZw0q4VwUPxEe1cXZgr+Wy5HZUQIotkZdg==",
- "dev": true,
"requires": {
"@types/node": ">6",
"nan": "^2.14.2",
@@ -5823,7 +5782,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "dev": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@@ -5833,7 +5791,6 @@
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "dev": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
@@ -5849,7 +5806,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "dev": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@@ -5861,7 +5817,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -5876,7 +5831,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -6063,7 +6017,6 @@
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz",
"integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==",
- "dev": true,
"requires": {
"detect-libc": "^1.0.3",
"expand-template": "^2.0.3",
@@ -6084,7 +6037,6 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "dev": true,
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@@ -6094,7 +6046,6 @@
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "dev": true,
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
@@ -6110,7 +6061,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "dev": true,
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@@ -6122,7 +6072,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -6137,7 +6086,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -6170,8 +6118,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"process-on-spawn": {
"version": "1.0.0",
@@ -6214,7 +6161,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dev": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@@ -6711,21 +6657,19 @@
"optional": true
},
"signal-exit": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz",
+ "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ=="
},
"simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "dev": true
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
},
"simple-get": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
- "dev": true,
"requires": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
@@ -7079,8 +7023,7 @@
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
- "dev": true
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"supports-color": {
"version": "7.1.0",
@@ -7106,8 +7049,7 @@
},
"tar": {
"version": "4.4.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
- "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
+ "resolved": "",
"dev": true,
"requires": {
"chownr": "^1.1.1",
@@ -7123,7 +7065,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
- "dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@@ -7135,7 +7076,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@@ -7148,7 +7088,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -7158,14 +7097,12 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
@@ -7620,8 +7557,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.3.2",
@@ -7765,7 +7701,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
- "dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
@@ -8239,9 +8174,9 @@
}
},
"ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"ansi-styles": {
@@ -11636,9 +11571,9 @@
"dev": true
},
"tmpl": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
- "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"to-fast-properties": {
diff --git a/e2e/window-test/package.json b/e2e/window-test/package.json
index 79a3c5cb..22162342 100644
--- a/e2e/window-test/package.json
+++ b/e2e/window-test/package.json
@@ -1,6 +1,6 @@
{
"name": "window-integration-tests",
- "version": "1.6.0",
+ "version": "1.7.0",
"description": "Integration tests to verify window handling",
"main": "main.js",
"author": {
diff --git a/e2e/window-test/test.js b/e2e/window-test/test.js
index fdb92d3c..e76cc23c 100644
--- a/e2e/window-test/test.js
+++ b/e2e/window-test/test.js
@@ -68,7 +68,7 @@ describe("getActiveWindow", () => {
it("should determine correct coordinates for our application after moving the window", async () => {
// GIVEN
const xPosition = 42;
- const yPosition = 23;
+ const yPosition = 25;
await app.browserWindow.setPosition(xPosition, yPosition);
await sleep(1000);
diff --git a/index.e2e.spec.ts b/index.e2e.spec.ts
deleted file mode 100644
index 7e48cab2..00000000
--- a/index.e2e.spec.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- assert,
- centerOf,
- down,
- Key,
- keyboard,
- mouse,
- Region,
- right,
- screen,
- sleep,
- straightTo,
-} from "./index";
-
-const openXfceMenu = async () => {
- await mouse.move(straightTo(centerOf(screen.find("menu.png"))));
- await mouse.leftClick();
- await mouse.leftClick();
-};
-
-const run = async (cmd: string) => {
- await keyboard.type(Key.LeftAlt, Key.F2);
- await keyboard.type(cmd);
- await keyboard.type(Key.Enter);
-};
-
-const calculate = async () => {
- await mouse.move(straightTo(centerOf(screen.find("plus.png"))));
- await mouse.leftClick();
- await mouse.move(straightTo(centerOf(screen.find("one.png"))));
- await mouse.leftClick();
- await mouse.move(straightTo(centerOf(screen.find("zero.png"))));
- await mouse.leftClick();
- await mouse.leftClick();
- await mouse.move(straightTo(centerOf(screen.find("equals.png"))));
- await mouse.leftClick();
-};
-
-const close = async () => {
- await mouse.move(straightTo(centerOf(screen.find("close.png"))));
- await mouse.leftClick();
-};
-
-describe("E2E tests", () => {
- afterEach(async () => {
- await keyboard.type(Key.LeftControl, Key.LeftAlt, Key.Left);
- });
-
- it("should throw on invalid images", async () => {
- jest.setTimeout(30000);
- await expect(screen.find("mouse.png")).rejects.toContain("Failed to load image");
- });
-
- it("should perform some calculations", async () => {
- jest.setTimeout(30000);
- screen.config.resourceDirectory = "./e2e/assets";
- await assert.isVisible("mouse.png");
- await assert.isVisible("desktop.png");
- await openXfceMenu();
- await run("gnome-calculator");
- await sleep(1500);
- await assert.isVisible("calculator.png");
- await keyboard.type("525");
- await calculate();
- await screen.waitFor("result.png");
- await close();
- });
-
- it("drag & drop", async () => {
- jest.setTimeout(60000);
- screen.config.resourceDirectory = "./e2e/assets";
-
- const expected = new Region(38, 585, 70, 86);
- const maxDiff = 1;
-
- await assert.isVisible("trash.png");
- await mouse.move(straightTo(centerOf(screen.find("trash.png"))));
- await mouse.drag(down(500));
- await mouse.move(right(100));
- await mouse.leftClick();
- const dest = await screen.waitFor("moved_trash.png");
- expect(Math.abs(dest.left - expected.left)).toBeLessThanOrEqual(maxDiff);
- expect(Math.abs(dest.top - expected.top)).toBeLessThanOrEqual(maxDiff);
- expect(Math.abs(dest.width - expected.width)).toBeLessThanOrEqual(maxDiff);
- expect(Math.abs(dest.height - expected.height)).toBeLessThanOrEqual(maxDiff);
- });
-});
diff --git a/index.ts b/index.ts
index 6b3df65a..db8b6d43 100644
--- a/index.ts
+++ b/index.ts
@@ -1,51 +1,72 @@
-import { NativeAdapter } from "./lib/adapter/native.adapter.class";
-import { VisionAdapter } from "./lib/adapter/vision.adapter.class";
-import { Assert } from "./lib/assert.class";
-import { Clipboard } from "./lib/clipboard.class";
-import { Keyboard } from "./lib/keyboard.class";
-import { Mouse } from "./lib/mouse.class";
-import { createMovementApi } from "./lib/movement.function";
-import { Screen } from "./lib/screen.class";
-import { LineHelper } from "./lib/util/linehelper.class";
-import { createWindowApi } from "./lib/window.function";
-
-export { jestMatchers } from "./lib/expect/jest.matcher.function";
-export { sleep } from "./lib/sleep.function";
-export { Image } from "./lib/image.class";
-export { Key } from "./lib/key.enum";
-export { Button } from "./lib/button.enum";
-export { centerOf, randomPointIn } from "./lib/location.function";
-export { LocationParameters } from "./lib/locationparameters.class";
-export { OptionalSearchParameters } from "./lib/optionalsearchparameters.class";
-export { linear } from "./lib/movementtype.function";
-export { Point } from "./lib/point.class";
-export { Region } from "./lib/region.class";
-export { Window } from "./lib/window.class";
-
-const screenActions = new VisionAdapter();
-const nativeActions = new NativeAdapter();
+import {AssertClass} from "./lib/assert.class";
+import {ClipboardClass} from "./lib/clipboard.class";
+import {KeyboardClass} from "./lib/keyboard.class";
+import {MouseClass} from "./lib/mouse.class";
+import {createMovementApi} from "./lib/movement.function";
+import {ScreenClass} from "./lib/screen.class";
+import {LineHelper} from "./lib/util/linehelper.class";
+import {createWindowApi} from "./lib/window.function";
+import providerRegistry from "./lib/provider/provider-registry.class";
+import {loadImageResource} from "./lib/imageResources.function";
+
+export {
+ AssertClass,
+ ClipboardClass,
+ KeyboardClass,
+ MouseClass,
+ ScreenClass,
+ providerRegistry
+}
+
+export {MatchRequest} from "./lib/match-request.class";
+export {MatchResult} from "./lib/match-result.class";
+export * from "./lib/provider";
+
+export {jestMatchers} from "./lib/expect/jest.matcher.function";
+export {sleep} from "./lib/sleep.function";
+export {Image} from "./lib/image.class";
+export {RGBA} from "./lib/rgba.class";
+export {Key} from "./lib/key.enum";
+export {Button} from "./lib/button.enum";
+export {centerOf, randomPointIn} from "./lib/location.function";
+export {OptionalSearchParameters} from "./lib/optionalsearchparameters.class";
+export {EasingFunction, linear} from "./lib/mouse-movement.function";
+export {Point} from "./lib/point.class";
+export {Region} from "./lib/region.class";
+export {Window} from "./lib/window.class";
+export {FileType} from "./lib/file-type.enum";
+export {ColorMode} from "./lib/colormode.enum";
+
const lineHelper = new LineHelper();
-const clipboard = new Clipboard(nativeActions);
-const keyboard = new Keyboard(nativeActions);
-const mouse = new Mouse(nativeActions);
-const screen = new Screen(screenActions);
-const assert = new Assert(screen);
+const clipboard = new ClipboardClass(providerRegistry);
+const keyboard = new KeyboardClass(providerRegistry);
+const mouse = new MouseClass(providerRegistry);
+const screen = new ScreenClass(providerRegistry);
+const assert = new AssertClass(screen);
+
+const {straightTo, up, down, left, right} = createMovementApi(providerRegistry, lineHelper);
+const {getWindows, getActiveWindow} = createWindowApi(providerRegistry);
+
+const loadImage = providerRegistry.getImageReader().load;
+const saveImage = providerRegistry.getImageWriter().store;
-const {straightTo, up, down, left, right} = createMovementApi(nativeActions, lineHelper);
-const {getWindows, getActiveWindow } = createWindowApi(nativeActions);
+const imageResource = (fileName: string) => loadImageResource(providerRegistry, screen.config.resourceDirectory, fileName);
export {
- clipboard,
- keyboard,
- mouse,
- screen,
- assert,
- straightTo,
- up,
- down,
- left,
- right,
- getWindows,
- getActiveWindow,
+ clipboard,
+ keyboard,
+ mouse,
+ screen,
+ assert,
+ straightTo,
+ up,
+ down,
+ left,
+ right,
+ getWindows,
+ getActiveWindow,
+ loadImage,
+ saveImage,
+ imageResource
};
diff --git a/lib/adapter/native.adapter.class.spec.ts b/lib/adapter/native.adapter.class.spec.ts
deleted file mode 100644
index ab0da5a4..00000000
--- a/lib/adapter/native.adapter.class.spec.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-import { Button } from "../button.enum";
-import { Key } from "../key.enum";
-import { Point } from "../point.class";
-import { NativeAdapter } from "./native.adapter.class";
-import ClipboardAction from "../provider/native/clipboardy-clipboard-action.class";
-import KeyboardAction from "../provider/native/libnut-keyboard-action.class";
-import MouseAction from "../provider/native/libnut-mouse-action.class";
-import WindowAction from "../provider/native/libnut-window-action.class";
-
-jest.mock("../provider/native/clipboardy-clipboard-action.class");
-jest.mock("../provider/native/libnut-mouse-action.class");
-jest.mock("../provider/native/libnut-keyboard-action.class");
-jest.mock("../provider/native/libnut-window-action.class");
-
-describe("NativeAdapter class", () => {
- describe("MouseAction", () => {
- it("should delegate calls to setMouseDelay", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const delay = 5;
-
- // WHEN
- SUT.setMouseDelay(delay);
-
- // THEN
- expect(mouseMock.setMouseDelay).toBeCalledTimes(1);
- expect(mouseMock.setMouseDelay).toBeCalledWith(delay);
- });
-
- it("should delegate calls to setMousePosition", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const newPosition = new Point(10, 10);
-
- // WHEN
- await SUT.setMousePosition(newPosition);
-
- // THEN
- expect(mouseMock.setMousePosition).toBeCalledTimes(1);
- expect(mouseMock.setMousePosition).toBeCalledWith(newPosition);
- });
-
- it("should delegate calls to currentMousePosition", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.currentMousePosition();
-
- // THEN
- expect(mouseMock.currentMousePosition).toBeCalledTimes(1);
- });
-
- it("should delegate calls to leftClick", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.leftClick();
-
- // THEN
- expect(mouseMock.leftClick).toBeCalledTimes(1);
- });
-
- it("should delegate calls to rightClick", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.rightClick();
-
- // THEN
- expect(mouseMock.rightClick).toBeCalledTimes(1);
- });
-
- it("should delegate calls to middleClick", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.middleClick();
-
- // THEN
- expect(mouseMock.middleClick).toBeCalledTimes(1);
- });
-
- it("should delegate calls to pressButton", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const buttonToPress = Button.LEFT;
-
- // WHEN
- await SUT.pressButton(buttonToPress);
-
- // THEN
- expect(mouseMock.pressButton).toBeCalledTimes(1);
- expect(mouseMock.pressButton).toBeCalledWith(buttonToPress);
- });
-
- it("should delegate calls to releaseButton", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const buttonToRelease = Button.LEFT;
-
- // WHEN
- await SUT.releaseButton(buttonToRelease);
-
- // THEN
- expect(mouseMock.releaseButton).toBeCalledTimes(1);
- expect(mouseMock.releaseButton).toBeCalledWith(buttonToRelease);
- });
- });
-
- describe("KeyboardAction", () => {
- it("should delegate calls to pressKey", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const keyToPress = Key.A;
-
- // WHEN
- await SUT.pressKey(keyToPress);
-
- // THEN
- expect(keyboardMock.pressKey).toBeCalledTimes(1);
- expect(keyboardMock.pressKey).toBeCalledWith(keyToPress);
- });
-
- it("should delegate calls to releaseButton", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const keyToRelease = Key.A;
-
- // WHEN
- await SUT.releaseKey(keyToRelease);
-
- // THEN
- expect(keyboardMock.releaseKey).toBeCalledTimes(1);
- expect(keyboardMock.releaseKey).toBeCalledWith(keyToRelease);
- });
-
- it("should delegate calls to click", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const keyToClick = Key.A;
-
- // WHEN
- await SUT.click(keyToClick);
-
- // THEN
- expect(keyboardMock.click).toBeCalledTimes(1);
- expect(keyboardMock.click).toBeCalledWith(keyToClick);
- });
-
- it("should delegate calls to type", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const stringToType = "testString";
-
- // WHEN
- await SUT.type(stringToType);
-
- // THEN
- expect(keyboardMock.type).toBeCalledTimes(1);
- expect(keyboardMock.type).toBeCalledWith(stringToType);
- });
- });
-
- describe("ClipboardAction", () => {
- it("should delegate calls to copy", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const stringToCopy = "testString";
-
- // WHEN
- await SUT.copy(stringToCopy);
-
- // THEN
- expect(clipboardMock.copy).toBeCalledTimes(1);
- expect(clipboardMock.copy).toBeCalledWith(stringToCopy);
- });
-
- it("should delegate calls to paste", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.paste();
-
- // THEN
- expect(clipboardMock.paste).toBeCalledTimes(1);
- });
- });
-
- describe("WindowAction", () => {
- it("should delegate calls to getActiveWindow", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.getActiveWindow();
-
- // THEN
- expect(windowMock.getActiveWindow).toBeCalledTimes(1);
- });
-
- it("should delegate calls to getWindows", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
-
- // WHEN
- await SUT.getWindows();
-
- // THEN
- expect(windowMock.getWindows).toBeCalledTimes(1);
- });
-
- it("should delegate calls to getWindowTitle", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const windowHandle = 123;
-
- // WHEN
- await SUT.getWindowTitle(windowHandle);
-
- // THEN
- expect(windowMock.getWindowTitle).toBeCalledTimes(1);
- });
-
- it("should delegate calls to getWindowRegion", async () => {
- // GIVEN
- const clipboardMock = new ClipboardAction();
- const keyboardMock = new KeyboardAction();
- const mouseMock = new MouseAction();
- const windowMock = new WindowAction();
- const SUT = new NativeAdapter(clipboardMock, keyboardMock, mouseMock, windowMock);
- const windowHandle = 123;
-
- // WHEN
- await SUT.getWindowRegion(windowHandle);
-
- // THEN
- expect(windowMock.getWindowRegion).toBeCalledTimes(1);
- });
- });
-});
diff --git a/lib/adapter/native.adapter.class.ts b/lib/adapter/native.adapter.class.ts
deleted file mode 100644
index 67122d97..00000000
--- a/lib/adapter/native.adapter.class.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import { Button } from "../button.enum";
-import { Key } from "../key.enum";
-import { Point } from "../point.class";
-import { ClipboardActionProvider } from "../provider/native/clipboard-action-provider.interface";
-import { KeyboardActionProvider } from "../provider/native/keyboard-action-provider.interface";
-import { MouseActionProvider } from "../provider/native/mouse-action-provider.interface";
-import { Region } from "../region.class";
-import { WindowActionProvider } from "../provider/native/window-action-provider.interface";
-import ClipboardAction from "../provider/native/clipboardy-clipboard-action.class";
-import KeyboardAction from "../provider/native/libnut-keyboard-action.class";
-import MouseAction from "../provider/native/libnut-mouse-action.class";
-import WindowAction from "../provider/native/libnut-window-action.class";
-
-/**
- * {@link NativeAdapter} serves as an abstraction layer for all OS level interactions.
- *
- * This allows to provide a high level interface for native actions,
- * without having to spread (possibly) multiple dependencies all over the code.
- * All actions which involve the OS are bundled in this adapter.
- */
-export class NativeAdapter {
- /**
- * {@link NativeAdapter} class constructor
- * @param clipboard {@link ClipboardActionProvider} instance used to interact with a systems clipboard (Default: {@link ClipboardAction})
- * @param keyboard {@link KeyboardActionProvider} instance used to interact with a systems keybaord (Default: {@link KeyboardAction})
- * @param mouse {@link MouseActionProvider} instance used to interact with a systems mouse (Default: {@link MouseAction})
- * @param window {@link WindowActionProvider} instance used to interact with a systems windows (Default: {@link WindowAction})
- */
- constructor(
- private clipboard: ClipboardActionProvider = new ClipboardAction(),
- private keyboard: KeyboardActionProvider = new KeyboardAction(),
- private mouse: MouseActionProvider = new MouseAction(),
- private window: WindowActionProvider = new WindowAction(),
- ) {}
-
- /**
- * {@link setMouseDelay} configures mouse speed for movement
- *
- * @param delay Mouse delay in milliseconds
- */
- public setMouseDelay(delay: number): void {
- this.mouse.setMouseDelay(delay);
- }
-
- /**
- * {@link setKeyboardDelay} configures keyboard delay between key presses
- *
- * @param delay The keyboard delay in milliseconds
- */
- public setKeyboardDelay(delay: number): void {
- this.keyboard.setKeyboardDelay(delay);
- }
-
- /**
- * {@link setMousePosition} changes the current mouse cursor position to a given {@link Point}
- *
- * @param p The new cursor position at {@link Point} p
- */
- public setMousePosition(p: Point): Promise {
- return this.mouse.setMousePosition(p);
- }
-
- /**
- * {@link currentMousePosition} returns the current mouse position
- *
- * @returns Current cursor position at a certain {@link Point}
- */
- public currentMousePosition(): Promise {
- return this.mouse.currentMousePosition();
- }
-
- /**
- * {@link leftClick} triggers a native left-click event via OS API
- */
- public leftClick(): Promise {
- return this.mouse.leftClick();
- }
-
- /**
- * {@link rightClick} triggers a native right-click event via OS API
- */
- public rightClick(): Promise {
- return this.mouse.rightClick();
- }
-
- /**
- * {@link middleClick} triggers a native middle-click event via OS API
- */
- public middleClick(): Promise {
- return this.mouse.middleClick();
- }
-
- /**
- * {@link pressButton} presses and holds a mouse {@link Button}
- *
- * @param btn The mouse {@link Button} to press
- */
- public pressButton(btn: Button): Promise {
- return this.mouse.pressButton(btn);
- }
-
- /**
- * {@link releaseButton} releases a mouse {@link Button} previously clicked via {@link pressButton}
- *
- * @param btn The mouse {@link Button} to release
- */
- public releaseButton(btn: Button): Promise {
- return this.mouse.releaseButton(btn);
- }
-
- /**
- * {@link type} types a given string via native keyboard events
- *
- * @param input The text to type
- */
- public type(input: string): Promise {
- return this.keyboard.type(input);
- }
-
- /**
- * {@link click} clicks a {@link Key} via native keyboard event
- *
- * @param keys Array of {@link Key}s to click
- */
- public click(...keys: Key[]): Promise {
- return this.keyboard.click(...keys);
- }
-
- /**
- * {@link pressKey} presses and holds a given {@link Key}
- *
- * @param keys Array of {@link Key}s to press and hold
- */
- public pressKey(...keys: Key[]): Promise {
- return this.keyboard.pressKey(...keys);
- }
-
- /**
- * {@link releaseKey} releases a {@link Key} previously presses via {@link pressKey}
- *
- * @param keys Array of {@link Key}s to release
- */
- public releaseKey(...keys: Key[]): Promise {
- return this.keyboard.releaseKey(...keys);
- }
-
- /**
- * {@link scrollUp} triggers an upwards mouse wheel scroll
- *
- * @param amount The amount of 'ticks' to scroll
- */
- public scrollUp(amount: number): Promise {
- return this.mouse.scrollUp(amount);
- }
-
- /**
- * {@link scrollDown} triggers a downward mouse wheel scroll
- *
- * @param amount The amount of 'ticks' to scroll
- */
- public scrollDown(amount: number): Promise {
- return this.mouse.scrollDown(amount);
- }
-
- /**
- * {@link scrollLeft} triggers a left mouse scroll
- *
- * @param amount The amount of 'ticks' to scroll
- */
- public scrollLeft(amount: number): Promise {
- return this.mouse.scrollLeft(amount);
- }
-
- /**
- * {@link scrollRight} triggers a right mouse scroll
- *
- * @param amount The amount of 'ticks' to scroll
- */
- public scrollRight(amount: number): Promise {
- return this.mouse.scrollRight(amount);
- }
-
- /**
- * {@link copy} copies a given text to the system clipboard
- *
- * @param text The text to copy
- */
- public copy(text: string): Promise {
- return this.clipboard.copy(text);
- }
-
- /**
- * {@link paste} pastes the current text on the system clipboard
- *
- * @returns The clipboard text
- */
- public paste(): Promise {
- return this.clipboard.paste();
- }
-
- public getWindows(): Promise {
- return this.window.getWindows();
- }
-
- /**
- * {@link getActiveWindow} returns the window handle of the currently active foreground window
- *
- * @returns The handle to the currently active foreground window
- */
- public getActiveWindow(): Promise {
- return this.window.getActiveWindow();
- }
-
- /**
- * {@link getWindowTitle} returns the title of a window addressed via its window handle
- *
- * @returns A string representing the title of a window addressed via its window handle
- */
- public getWindowTitle(windowHandle: number): Promise {
- return this.window.getWindowTitle(windowHandle);
- }
-
- /**
- * {@link getWindowRegion} returns a {@link Region} object representing the size and position of the window addressed via its window handle
- *
- * @returns The {@link Region} occupied by the window addressed via its window handle
- */
- public getWindowRegion(windowHandle: number): Promise {
- return this.window.getWindowRegion(windowHandle);
- }
-}
diff --git a/lib/adapter/vision.adapter.class.spec.ts b/lib/adapter/vision.adapter.class.spec.ts
deleted file mode 100644
index df79ce7e..00000000
--- a/lib/adapter/vision.adapter.class.spec.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Image } from "../image.class";
-import { MatchRequest } from "../match-request.class";
-import ScreenAction from "../provider/native/libnut-screen-action.class";
-import TemplateMatchingFinder from "../provider/opencv/template-matching-finder.class";
-import { Region } from "../region.class";
-import { VisionAdapter } from "./vision.adapter.class";
-
-jest.mock("../provider/opencv/template-matching-finder.class");
-jest.mock("../provider/native/libnut-screen-action.class");
-
-describe("VisionAdapter class", () => {
- it("should delegate calls to grabScreen", () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
-
- // WHEN
- SUT.grabScreen();
-
- // THEN
- expect(screenMock.grabScreen).toBeCalledTimes(1);
- });
-
- it("should delegate calls to grabScreenRegion", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
- const screenRegion = new Region(0, 0, 100, 100);
-
- // WHEN
- await SUT.grabScreenRegion(screenRegion);
-
- // THEN
- expect(screenMock.grabScreenRegion).toBeCalledTimes(1);
- expect(screenMock.grabScreenRegion).toBeCalledWith(screenRegion);
- });
-
- it("should delegate calls to highlightScreenRegion", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
- const screenRegion = new Region(0, 0, 100, 100);
- const opacity = 0.25;
- const duration = 1;
-
- // WHEN
- await SUT.highlightScreenRegion(screenRegion, duration, opacity);
-
- // THEN
- expect(screenMock.highlightScreenRegion).toBeCalledTimes(1);
- expect(screenMock.highlightScreenRegion).toBeCalledWith(screenRegion, duration, opacity);
- });
-
- it("should delegate calls to screenWidth", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
-
- // WHEN
- await SUT.screenWidth();
-
- // THEN
- expect(screenMock.screenWidth).toBeCalledTimes(1);
- });
-
- it("should delegate calls to screenHeight", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
-
- // WHEN
- await SUT.screenHeight();
-
- // THEN
- expect(screenMock.screenHeight).toBeCalledTimes(1);
- });
-
- it("should delegate calls to screenSize", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const screenMock = new ScreenAction();
- const SUT = new VisionAdapter(finderMock, screenMock);
-
- // WHEN
- await SUT.screenSize();
-
- // THEN
- expect(screenMock.screenSize).toBeCalledTimes(1);
- });
-
- it("should delegate calls to findImage", async () => {
- // GIVEN
- const finderMock = new TemplateMatchingFinder();
- const SUT = new VisionAdapter(finderMock);
- const request = new MatchRequest(
- new Image(100, 100, new ArrayBuffer(0), 3),
- "foo",
- new Region(0, 0, 100, 100),
- 0.99,
- true);
-
- // WHEN
- await SUT.findOnScreenRegion(request);
-
- expect(finderMock.findMatch).toBeCalledTimes(1);
- expect(finderMock.findMatch).toBeCalledWith(request);
- });
-});
diff --git a/lib/adapter/vision.adapter.class.ts b/lib/adapter/vision.adapter.class.ts
deleted file mode 100644
index ad5f51ae..00000000
--- a/lib/adapter/vision.adapter.class.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { Image } from "../image.class";
-import { MatchRequest } from "../match-request.class";
-import { MatchResult } from "../match-result.class";
-import ScreenAction from "../provider/native/libnut-screen-action.class";
-import { ScreenActionProvider } from "../provider/native/screen-action-provider.interface";
-import { DataSink } from "../provider/opencv/data-sink.interface";
-import { FinderInterface } from "../provider/opencv/finder.interface";
-import { ImageWriter } from "../provider/opencv/image-writer.class";
-import TemplateMatchingFinder from "../provider/opencv/template-matching-finder.class";
-import { Region } from "../region.class";
-
-/**
- * {@link VisionAdapter} serves as an abstraction layer for all image based interactions.
- *
- * This allows to provide a high level interface for image based actions,
- * without having to spread (possibly) multiple dependencies all over the code.
- * All actions which involve screenshots / images are bundled in this adapter.
- */
-export class VisionAdapter {
- /**
- * {@link VisionAdapter} class constructor
- * @param finder A {@link FinderInterface} instance used for on-screen image detection (Default: {@link TemplateMatchingFinder})
- * @param screen A {@link ScreenActionProvider} instance used to retrieve screen data (Default: {@link ScreenAction})
- * @param dataSink A {@link DataSink} instance used to write output data to disk (Default: {@link ImageWriter})
- */
- constructor(
- private finder: FinderInterface = new TemplateMatchingFinder(),
- private screen: ScreenActionProvider = new ScreenAction(),
- private dataSink: DataSink = new ImageWriter()
- ) {
- }
-
- /**
- * {@link grabScreen} will return an {@link Image} containing the current screen image
- *
- * @returns An {@link Image} which will contain screenshot data as well as dimensions
- */
- public grabScreen(): Promise {
- return this.screen.grabScreen();
- }
-
- /**
- * {@link grabScreenRegion} essentially does the same as grabScreen, but only returns a specified {@link Region}
- *
- * @param region The screen {@link Region} we want to grab
- * @returns An {@link Image} which will contain screenshot data of the specified {@link Region} as well as dimensions
- */
- public grabScreenRegion(region: Region): Promise {
- return this.screen.grabScreenRegion(region);
- }
-
- /**
- * {@link highlightScreenRegion} highlights a screen {@link Region} for a given duration by overlaying it with an opaque window
- *
- * @param region The {@link Region} to highlight
- * @param duration The highlight duration
- * @param opacity Overlay opacity
- */
- public highlightScreenRegion(region: Region, duration: number, opacity: number): Promise {
- return this.screen.highlightScreenRegion(region, duration, opacity);
- }
-
- /**
- * {@link findOnScreenRegion} will search for a given pattern inside a {@link Region} of the main screen
- * If multiple possible occurrences are found, the one with the highest probability is returned.
- * For matchProbability < 0.99 the search will be performed on grayscale images.
- *
- * @param matchRequest A {@link MatchRequest} which holds all required matching data
- * @returns {@link MatchResult} containing location and probability of a possible match
- */
- public async findOnScreenRegion(
- matchRequest: MatchRequest,
- ): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- const matchResult = await this.finder.findMatch(matchRequest);
- resolve(matchResult);
- } catch (e) {
- reject(e);
- }
- });
- }
-
- /**
- * {@link screenWidth} returns the main screens width as reported by the OS.
- * Please notice that on e.g. Apples Retina display the reported width
- * and the actual pixel size may differ
- *
- * @returns The main screens width as reported by the OS
- */
- public screenWidth(): Promise {
- return this.screen.screenWidth();
- }
-
- /**
- * {@link screenHeight} returns the main screens height as reported by the OS.
- * Please notice that on e.g. Apples Retina display the reported width
- * and the actual pixel size may differ
- *
- * @returns The main screens height as reported by the OS
- */
- public screenHeight(): Promise {
- return this.screen.screenHeight();
- }
-
- /**
- * {@link screenSize} returns a {@link Region} object with the main screens size.
- * Please note that on e.g. Apples Retina display the reported width
- * and the actual pixel size may differ
- *
- * @returns A {@link Region} object representing the size of a systems main screen
- */
- public screenSize(): Promise {
- return this.screen.screenSize();
- }
-
- /**
- * {@link saveImage} saves an {@link Image} to a given path on disk.
- *
- * @param image The {@link Image} to store
- * @param path The path where to store the image
- */
- public saveImage(image: Image, path: string): Promise {
- return (this.dataSink as ImageWriter).store(image, path);
- }
-}
diff --git a/lib/assert.class.spec.ts b/lib/assert.class.spec.ts
index 3a022346..8f3b1e0b 100644
--- a/lib/assert.class.spec.ts
+++ b/lib/assert.class.spec.ts
@@ -1,79 +1,93 @@
-import { VisionAdapter } from "./adapter/vision.adapter.class";
-import { Assert } from "./assert.class";
-import { Region } from "./region.class";
-import { Screen } from "./screen.class";
-
-jest.mock("./adapter/native.adapter.class");
-jest.mock("./adapter/vision.adapter.class");
+import {AssertClass} from "./assert.class";
+import {Region} from "./region.class";
+import {ScreenClass} from "./screen.class";
+import providerRegistry from "./provider/provider-registry.class";
+import {Image} from "../index";
+import {mockPartial} from "sneer";
+
+jest.mock('jimp', () => {
+});
jest.mock("./screen.class");
+const needleId = "needleId";
+
describe("Assert", () => {
- it("isVisible should not throw if a match is found.", async () => {
- // GIVEN
- Screen.prototype.find = jest.fn(() => Promise.resolve(new Region(0, 0, 100, 100)));
- const screenMock = new Screen(new VisionAdapter());
- const SUT = new Assert(screenMock);
- const needle = "foo";
-
- // WHEN
-
- // THEN
- await expect(SUT.isVisible(needle)).resolves.not.toThrowError();
- });
-
- it("isVisible should throw if a match is found.", async () => {
- // GIVEN
- Screen.prototype.find = jest.fn(() => Promise.reject("foo"));
- const screenMock = new Screen(new VisionAdapter());
- const SUT = new Assert(screenMock);
- const needle = "foo";
-
- // WHEN
-
- // THEN
- await expect(SUT.isVisible(needle)).rejects.toThrowError(`Element '${needle}' not found`);
- });
-
- it("isVisible should throw if a match is found.", async () => {
- // GIVEN
- Screen.prototype.find = jest.fn(() => Promise.reject("foo"));
- const screenMock = new Screen(new VisionAdapter());
- const SUT = new Assert(screenMock);
- const searchRegion = new Region(10, 10, 10, 10);
- const needle = "foo";
-
- // WHEN
-
- // THEN
- await expect(SUT
- .isVisible(needle, searchRegion))
- .rejects.toThrowError(`Element '${needle}' not found in region ${searchRegion.toString()}`
- );
- });
-
- it("isNotVisible should throw if a match is found.", async () => {
- // GIVEN
- Screen.prototype.find = jest.fn(() => Promise.resolve(new Region(0, 0, 100, 100)));
- const screenMock = new Screen(new VisionAdapter());
- const SUT = new Assert(screenMock);
- const needle = "foo";
-
- // WHEN
-
- // THEN
- await expect(SUT.notVisible(needle)).rejects.toThrowError(`'${needle}' is visible`);
- });
-
- it("isVisible should throw if a match is found.", async () => {
- // GIVEN
- Screen.prototype.find = jest.fn(() => Promise.reject("foo"));
- const screenMock = new Screen(new VisionAdapter());
- const SUT = new Assert(screenMock);
- const needle = "foo";
-
- // WHEN
-
- // THEN
- await expect(SUT.notVisible(needle)).resolves.not.toThrowError();
- });
+ it("isVisible should not throw if a match is found.", async () => {
+ // GIVEN
+ ScreenClass.prototype.find = jest.fn(() => Promise.resolve(new Region(0, 0, 100, 100)));
+ const screenMock = new ScreenClass(providerRegistry);
+ const SUT = new AssertClass(screenMock);
+ const needle = mockPartial({
+ id: needleId
+ });
+
+ // WHEN
+
+ // THEN
+ await expect(SUT.isVisible(needle)).resolves.not.toThrowError();
+ });
+
+ it("isVisible should throw if a match is found.", async () => {
+ // GIVEN
+ ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
+ const screenMock = new ScreenClass(providerRegistry);
+ const SUT = new AssertClass(screenMock);
+ const needle = mockPartial({
+ id: needleId
+ });
+
+ // WHEN
+
+ // THEN
+ await expect(SUT.isVisible(needle)).rejects.toThrowError(`Element '${needle.id}' not found`);
+ });
+
+ it("isVisible should throw if a match is found.", async () => {
+ // GIVEN
+ ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
+ const screenMock = new ScreenClass(providerRegistry);
+ const SUT = new AssertClass(screenMock);
+ const searchRegion = new Region(10, 10, 10, 10);
+ const needle = mockPartial({
+ id: needleId
+ });
+
+ // WHEN
+
+ // THEN
+ await expect(SUT
+ .isVisible(needle, searchRegion))
+ .rejects.toThrowError(`Element '${needle.id}' not found in region ${searchRegion.toString()}`
+ );
+ });
+
+ it("isNotVisible should throw if a match is found.", async () => {
+ // GIVEN
+ ScreenClass.prototype.find = jest.fn(() => Promise.resolve(new Region(0, 0, 100, 100)));
+ const screenMock = new ScreenClass(providerRegistry);
+ const SUT = new AssertClass(screenMock);
+ const needle = mockPartial({
+ id: needleId
+ });
+
+ // WHEN
+
+ // THEN
+ await expect(SUT.notVisible(needle)).rejects.toThrowError(`'${needle.id}' is visible`);
+ });
+
+ it("isVisible should throw if a match is found.", async () => {
+ // GIVEN
+ ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
+ const screenMock = new ScreenClass(providerRegistry);
+ const SUT = new AssertClass(screenMock);
+ const needle = mockPartial({
+ id: needleId
+ });
+
+ // WHEN
+
+ // THEN
+ await expect(SUT.notVisible(needle)).resolves.not.toThrowError();
+ });
});
diff --git a/lib/assert.class.ts b/lib/assert.class.ts
index 14cb0cd2..6409e791 100644
--- a/lib/assert.class.ts
+++ b/lib/assert.class.ts
@@ -1,36 +1,42 @@
-import { LocationParameters } from "./locationparameters.class";
-import { Region } from "./region.class";
-import { Screen } from "./screen.class";
+import {Region} from "./region.class";
+import {ScreenClass} from "./screen.class";
+import {FirstArgumentType} from "./typings";
+import {OptionalSearchParameters} from "./optionalsearchparameters.class";
-export class Assert {
- constructor(private screen: Screen) {}
+export class AssertClass {
+ constructor(private screen: ScreenClass) {
+ }
+
+ public async isVisible(needle: FirstArgumentType, searchRegion?: Region, confidence?: number) {
+ const identifier = (await needle).id;
- public async isVisible(pathToNeedle: string, searchRegion?: Region, confidence?: number) {
- try {
- await this.screen.find(
- pathToNeedle,
- {searchRegion, confidence} as LocationParameters,
- );
- } catch (err) {
- if (searchRegion !== undefined) {
- throw new Error(
- `Element '${pathToNeedle}' not found in region ${searchRegion.toString()}`,
- );
- } else {
- throw new Error(`Element '${pathToNeedle}' not found`);
- }
+ try {
+ await this.screen.find(
+ needle,
+ {searchRegion, confidence} as OptionalSearchParameters,
+ );
+ } catch (err) {
+ if (searchRegion !== undefined) {
+ throw new Error(
+ `Element '${identifier}' not found in region ${searchRegion.toString()}. Reason: ${err}`,
+ );
+ } else {
+ throw new Error(`Element '${identifier}' not found. Reason: ${err}`);
+ }
+ }
}
- }
- public async notVisible(pathToNeedle: string, searchRegion?: Region, confidence?: number) {
- try {
- await this.screen.find(
- pathToNeedle,
- {searchRegion, confidence} as LocationParameters,
- );
- } catch (err) {
- return;
+ public async notVisible(needle: FirstArgumentType, searchRegion?: Region, confidence?: number) {
+ const identifier = (await needle).id;
+
+ try {
+ await this.screen.find(
+ needle,
+ {searchRegion, confidence} as OptionalSearchParameters,
+ );
+ } catch (err) {
+ return;
+ }
+ throw new Error(`'${identifier}' is visible`);
}
- throw new Error(`'${pathToNeedle}' is visible`);
- }
}
diff --git a/lib/clipboard.class.e2e.spec.ts b/lib/clipboard.class.e2e.spec.ts
index 5545ba3b..d4e35f1d 100644
--- a/lib/clipboard.class.e2e.spec.ts
+++ b/lib/clipboard.class.e2e.spec.ts
@@ -1,10 +1,9 @@
-import {NativeAdapter} from "./adapter/native.adapter.class";
-import {Clipboard} from "./clipboard.class";
+import {ClipboardClass} from "./clipboard.class";
+import providerRegistry from "./provider/provider-registry.class";
describe("Clipboard class", () => {
it("should paste copied input from system clipboard.", async () => {
- const adapterMock = new NativeAdapter();
- const SUT = new Clipboard(adapterMock);
+ const SUT = new ClipboardClass(providerRegistry);
const textToCopy = "bar";
diff --git a/lib/clipboard.class.spec.ts b/lib/clipboard.class.spec.ts
index f957a56a..c0119bc0 100644
--- a/lib/clipboard.class.spec.ts
+++ b/lib/clipboard.class.spec.ts
@@ -1,29 +1,47 @@
-import {NativeAdapter} from "./adapter/native.adapter.class";
-import {Clipboard} from "./clipboard.class";
+import {ClipboardClass} from "./clipboard.class";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {mockPartial} from "sneer";
+import {ClipboardProviderInterface} from "./provider";
-jest.mock("./adapter/native.adapter.class");
+jest.mock('jimp', () => {
+});
beforeEach(() => {
- jest.resetAllMocks();
+ jest.clearAllMocks();
});
-describe("Clipboard class", () => {
- it("should call the native adapters copy method.", () => {
- const adapterMock = new NativeAdapter();
- const SUT = new Clipboard(adapterMock);
+const providerRegistryMock = mockPartial({})
+describe("Clipboard class", () => {
+ it("should call providers copy method.", () => {
+ // GIVEN
+ const SUT = new ClipboardClass(providerRegistryMock);
+ const copyMock = jest.fn();
+ providerRegistryMock.getClipboard = jest.fn(() => mockPartial({
+ copy: copyMock
+ }));
const textToCopy = "bar";
+ // WHEN
SUT.copy(textToCopy);
- expect(adapterMock.copy).toHaveBeenCalledTimes(1);
- expect(adapterMock.copy).toHaveBeenCalledWith(textToCopy);
+
+ // THEN
+ expect(copyMock).toHaveBeenCalledTimes(1);
+ expect(copyMock).toHaveBeenCalledWith(textToCopy);
});
- it("should call the native adapters paste method.", () => {
- const adapterMock = new NativeAdapter();
- const SUT = new Clipboard(adapterMock);
+ it("should call providers paste method.", () => {
+ // GIVEN
+ const SUT = new ClipboardClass(providerRegistryMock);
+ const pasteMock = jest.fn();
+ providerRegistryMock.getClipboard = jest.fn(() => mockPartial({
+ paste: pasteMock
+ }));
+ // WHEN
SUT.paste();
- expect(adapterMock.paste).toHaveBeenCalledTimes(1);
+
+ // THEN
+ expect(pasteMock).toHaveBeenCalledTimes(1);
});
});
diff --git a/lib/clipboard.class.ts b/lib/clipboard.class.ts
index c116c523..67aa3744 100644
--- a/lib/clipboard.class.ts
+++ b/lib/clipboard.class.ts
@@ -1,14 +1,14 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-
/**
- * {@link Clipboard} class gives access to a systems clipboard
+ * {@link ClipboardClass} class gives access to a systems clipboard
*/
-export class Clipboard {
+import {ProviderRegistry} from "./provider/provider-registry.class";
+
+export class ClipboardClass {
/**
- * {@link Clipboard} class constructor
- * @param nativeAdapter {@link NativeAdapter} instance used to access OS API
+ * {@link ClipboardClass} class constructor
+ * @param providerRegistry
*/
- constructor(private nativeAdapter: NativeAdapter) {
+ constructor(private providerRegistry: ProviderRegistry) {
}
/**
@@ -16,13 +16,13 @@ export class Clipboard {
* @param text The text to copy
*/
public copy(text: string): Promise {
- return this.nativeAdapter.copy(text);
+ return this.providerRegistry.getClipboard().copy(text);
}
/**
* {@link paste} returns the current content of the system clipboard (limited to text)
*/
public paste(): Promise {
- return this.nativeAdapter.paste();
+ return this.providerRegistry.getClipboard().paste();
}
}
diff --git a/lib/colormode.enum.ts b/lib/colormode.enum.ts
new file mode 100644
index 00000000..ce0b0434
--- /dev/null
+++ b/lib/colormode.enum.ts
@@ -0,0 +1,7 @@
+/**
+ * The {@link ColorMode} enum is used to specify the color mode of an {@link Image}
+ */
+export enum ColorMode {
+ BGR,
+ RGB
+}
\ No newline at end of file
diff --git a/lib/expect/jest.matcher.function.ts b/lib/expect/jest.matcher.function.ts
index 32690746..9477f27d 100644
--- a/lib/expect/jest.matcher.function.ts
+++ b/lib/expect/jest.matcher.function.ts
@@ -1,21 +1,23 @@
-import { Point } from "../point.class";
-import { Region } from "../region.class";
-import { toBeAt } from "./matchers/toBeAt.function";
-import { toBeIn } from "./matchers/toBeIn.function";
-import { toShow } from "./matchers/toShow.function";
+import {Point} from "../point.class";
+import {Region} from "../region.class";
+import {toBeAt} from "./matchers/toBeAt.function";
+import {toBeIn} from "./matchers/toBeIn.function";
+import {toShow} from "./matchers/toShow.function";
+import {FirstArgumentType} from "../typings";
+import {ScreenClass} from "../screen.class";
declare global {
- namespace jest {
- interface Matchers {
- toBeAt: (position: Point) => {};
- toBeIn: (region: Region) => {};
- toShow: (needle: string, confidence?: number) => {};
+ namespace jest {
+ interface Matchers {
+ toBeAt: (position: Point) => {};
+ toBeIn: (region: Region) => {};
+ toShow: (needle: FirstArgumentType, confidence?: number) => {};
+ }
}
- }
}
export const jestMatchers = {
- toBeAt,
- toBeIn,
- toShow,
+ toBeAt,
+ toBeIn,
+ toShow,
};
diff --git a/lib/expect/matchers/toBeAt.function.e2e.spec.ts b/lib/expect/matchers/toBeAt.function.e2e.spec.ts
index ef4f2dcf..c40ebb63 100644
--- a/lib/expect/matchers/toBeAt.function.e2e.spec.ts
+++ b/lib/expect/matchers/toBeAt.function.e2e.spec.ts
@@ -2,6 +2,8 @@ import { mouse } from "../../../index";
import { Point } from "../../point.class";
import { toBeAt } from "./toBeAt.function";
+jest.mock('jimp', () => {});
+
const targetPoint = new Point(100, 100);
describe(".toBeAt", () => {
diff --git a/lib/expect/matchers/toBeAt.function.ts b/lib/expect/matchers/toBeAt.function.ts
index 25fe4785..e0bd7cd4 100644
--- a/lib/expect/matchers/toBeAt.function.ts
+++ b/lib/expect/matchers/toBeAt.function.ts
@@ -1,7 +1,7 @@
-import { Mouse } from "../../mouse.class";
+import { MouseClass } from "../../mouse.class";
import { Point } from "../../point.class";
-export const toBeAt = async (received: Mouse, position: Point) => {
+export const toBeAt = async (received: MouseClass, position: Point) => {
const currentPosition = await received.getPosition();
const success =
diff --git a/lib/expect/matchers/toBeIn.function.e2e.spec.ts b/lib/expect/matchers/toBeIn.function.e2e.spec.ts
index 19d0144f..ba3f2501 100644
--- a/lib/expect/matchers/toBeIn.function.e2e.spec.ts
+++ b/lib/expect/matchers/toBeIn.function.e2e.spec.ts
@@ -3,6 +3,8 @@ import { Point } from "../../point.class";
import { Region } from "../../region.class";
import { toBeIn } from "./toBeIn.function";
+jest.mock('jimp', () => {});
+
const targetPoint = new Point(400, 400);
describe(".toBeIn", () => {
diff --git a/lib/expect/matchers/toBeIn.function.ts b/lib/expect/matchers/toBeIn.function.ts
index 5fc3b3e8..372ec64e 100644
--- a/lib/expect/matchers/toBeIn.function.ts
+++ b/lib/expect/matchers/toBeIn.function.ts
@@ -1,7 +1,7 @@
-import { Mouse } from "../../mouse.class";
+import { MouseClass } from "../../mouse.class";
import { Region } from "../../region.class";
-export const toBeIn = async (received: Mouse, region: Region) => {
+export const toBeIn = async (received: MouseClass, region: Region) => {
const currentPosition = await received.getPosition();
const inX =
diff --git a/lib/expect/matchers/toShow.function.ts b/lib/expect/matchers/toShow.function.ts
index d50da7f4..3b3c3848 100644
--- a/lib/expect/matchers/toShow.function.ts
+++ b/lib/expect/matchers/toShow.function.ts
@@ -1,26 +1,28 @@
-import { LocationParameters } from "../../locationparameters.class";
-import { Screen } from "../../screen.class";
+import {ScreenClass} from "../../screen.class";
+import {FirstArgumentType} from "../../typings";
+import {OptionalSearchParameters} from "../../optionalsearchparameters.class";
export const toShow = async (
- received: Screen,
- needle: string,
- confidence?: number,
+ received: ScreenClass,
+ needle: FirstArgumentType,
+ confidence?: number,
) => {
- let locationParams;
- if (confidence) {
- locationParams = new LocationParameters();
- locationParams.confidence = confidence;
- }
- try {
- await received.find(needle, locationParams);
- return {
- message: () => `Expected screen to not show ${needle}`,
- pass: true,
- };
- } catch (err) {
- return {
- message: () => `Screen is not showing ${needle}: ${err}`,
- pass: false,
- };
- }
+ let locationParams;
+ if (confidence) {
+ locationParams = new OptionalSearchParameters();
+ locationParams.confidence = confidence;
+ }
+ const identifier = (await needle).id;
+ try {
+ await received.find(needle, locationParams);
+ return {
+ message: () => `Expected screen to not show ${identifier}`,
+ pass: true,
+ };
+ } catch (err) {
+ return {
+ message: () => `Screen is not showing ${identifier}: ${err}`,
+ pass: false,
+ };
+ }
};
diff --git a/lib/image.class.spec.ts b/lib/image.class.spec.ts
index 0b7740c6..78823179 100644
--- a/lib/image.class.spec.ts
+++ b/lib/image.class.spec.ts
@@ -1,30 +1,68 @@
-import { Image } from "./image.class";
+import {Image} from "./image.class";
+import {imageToJimp} from "./provider/io/imageToJimp.function";
+import {ColorMode} from "./colormode.enum";
+
+jest.mock("./provider/io/imageToJimp.function", () => {
+ return {
+ imageToJimp: jest.fn()
+ }
+});
+
+afterEach(() => {
+ jest.resetAllMocks();
+});
describe("Image class", () => {
- it("should return alphachannel = true for > 3 channels", () => {
- const SUT = new Image(200, 200, 123, 4);
- expect(SUT.hasAlphaChannel).toBeTruthy();
- });
-
- it("should return alphachannel = false for <= 3 channels", () => {
- const SUT = new Image(200, 200, 123, 3);
- expect(SUT.hasAlphaChannel).toBeFalsy();
- });
- it("should return alphachannel = false for <= 3 channels", () => {
- const SUT = new Image(200, 200, 123, 2);
- expect(SUT.hasAlphaChannel).toBeFalsy();
- });
- it("should return alphachannel = false for <= 3 channels", () => {
- const SUT = new Image(200, 200, 123, 1);
- expect(SUT.hasAlphaChannel).toBeFalsy();
- });
-
- it("should throw for <= 0 channels", () => {
- expect(() => new Image(200, 200, 123, 0)).toThrowError("Channel <= 0");
- });
-
- it("should have a default pixel density of 1.0", () => {
- const SUT = new Image(200, 200, 123, 1);
- expect(SUT.pixelDensity).toEqual({ scaleX: 1.0, scaleY: 1.0 });
- });
+ it("should return alphachannel = true for > 3 channels", () => {
+ const SUT = new Image(200, 200, Buffer.from([123]), 4, "id");
+ expect(SUT.hasAlphaChannel).toBeTruthy();
+ });
+
+ it("should return alphachannel = false for <= 3 channels", () => {
+ const SUT = new Image(200, 200, Buffer.from([123]), 3, "id");
+ expect(SUT.hasAlphaChannel).toBeFalsy();
+ });
+ it("should return alphachannel = false for <= 3 channels", () => {
+ const SUT = new Image(200, 200, Buffer.from([123]), 2, "id");
+ expect(SUT.hasAlphaChannel).toBeFalsy();
+ });
+ it("should return alphachannel = false for <= 3 channels", () => {
+ const SUT = new Image(200, 200, Buffer.from([123]), 1, "id");
+ expect(SUT.hasAlphaChannel).toBeFalsy();
+ });
+
+ it("should throw for <= 0 channels", () => {
+ expect(() => new Image(200, 200, Buffer.from([123]), 0, "id")).toThrowError("Channel <= 0");
+ });
+
+ it("should have a default pixel density of 1.0", () => {
+ const SUT = new Image(200, 200, Buffer.from([123]), 1, "id");
+ expect(SUT.pixelDensity).toEqual({scaleX: 1.0, scaleY: 1.0});
+ });
+
+ describe("Colormode", () => {
+ it("should not try to convert an image to BGR if it already has the correct color mode", async () => {
+ // GIVEN
+ const bgrImage = new Image(100, 100, Buffer.from([]), 3, "testImage");
+
+ // WHEN
+ const convertedImage = await bgrImage.toBGR();
+
+ // THEN
+ expect(convertedImage).toBe(bgrImage);
+ expect(imageToJimp).not.toBeCalledTimes(1)
+ });
+
+ it("should not try to convert an image to RGB if it already has the correct color mode", async () => {
+ // GIVEN
+ const rgbImage = new Image(100, 100, Buffer.from([]), 3, "testImage", ColorMode.RGB);
+
+ // WHEN
+ const convertedImage = await rgbImage.toRGB();
+
+ // THEN
+ expect(convertedImage).toBe(rgbImage);
+ expect(imageToJimp).not.toBeCalledTimes(1)
+ });
+ });
});
diff --git a/lib/image.class.ts b/lib/image.class.ts
index 1e511335..4fc1a154 100644
--- a/lib/image.class.ts
+++ b/lib/image.class.ts
@@ -1,34 +1,72 @@
+import {imageToJimp} from "./provider/io/imageToJimp.function";
+import {ColorMode} from "./colormode.enum";
+
/**
* The {@link Image} class represents generic image data
*/
export class Image {
- /**
- * {@link Image} class constructor
- * @param width {@link Image} width in pixels
- * @param height {@link Image} height in pixels
- * @param data Generic {@link Image} data
- * @param channels Amount of {@link Image} channels
- * @param pixelDensity Object containing scale info to work with e.g. Retina display data where the reported display size and pixel size differ (Default: {scaleX: 1.0, scaleY: 1.0})
- */
- constructor(
- public readonly width: number,
- public readonly height: number,
- public readonly data: any,
- public readonly channels: number,
- public readonly pixelDensity: { scaleX: number; scaleY: number } = {
- scaleX: 1.0,
- scaleY: 1.0,
- },
- ) {
- if (channels <= 0) {
- throw new Error("Channel <= 0");
+ /**
+ * {@link Image} class constructor
+ * @param width {@link Image} width in pixels
+ * @param height {@link Image} height in pixels
+ * @param data Generic {@link Image} data
+ * @param channels Amount of {@link Image} channels
+ * @param id Image identifier
+ * @param colorMode An images color mode, defaults to {@link ColorMode.BGR}
+ * @param pixelDensity Object containing scale info to work with e.g. Retina display data where the reported display size and pixel size differ (Default: {scaleX: 1.0, scaleY: 1.0})
+ */
+ constructor(
+ public readonly width: number,
+ public readonly height: number,
+ public readonly data: Buffer,
+ public readonly channels: number,
+ public readonly id: string,
+ public readonly colorMode: ColorMode = ColorMode.BGR,
+ public readonly pixelDensity: { scaleX: number; scaleY: number } = {
+ scaleX: 1.0,
+ scaleY: 1.0,
+ },
+ ) {
+ if (channels <= 0) {
+ throw new Error("Channel <= 0");
+ }
+ }
+
+ /**
+ * {@link hasAlphaChannel} return true if an {@link Image} has an additional (fourth) alpha channel
+ */
+ public get hasAlphaChannel() {
+ return this.channels > 3;
+ }
+
+ /**
+ * {@link toRGB} converts an {@link Image} from BGR color mode (default within nut.js) to RGB
+ */
+ public async toRGB(): Promise {
+ if (this.colorMode === ColorMode.RGB) {
+ return this;
+ }
+ const rgbImage = imageToJimp(this);
+ return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.RGB, this.pixelDensity);
+ }
+
+ /**
+ * {@link toBGR} converts an {@link Image} from RGB color mode to RGB
+ */
+ public async toBGR(): Promise {
+ if (this.colorMode === ColorMode.BGR) {
+ return this;
+ }
+ const rgbImage = imageToJimp(this);
+ return new Image(this.width, this.height, rgbImage.bitmap.data, this.channels, this.id, ColorMode.BGR, this.pixelDensity);
}
- }
- /**
- * {@link hasAlphaChannel} return true if an {@link Image} has an additional (fourth) alpha channel
- */
- public get hasAlphaChannel() {
- return this.channels > 3;
- }
+ /**
+ * {@link fromRGBData} creates an {@link Image} from provided RGB data
+ */
+ public static fromRGBData(width: number, height: number, data: Buffer, channels: number, id: string): Image {
+ const rgbImage = new Image(width, height, data, channels, id);
+ const jimpImage = imageToJimp(rgbImage);
+ return new Image(width, height, jimpImage.bitmap.data, channels, id);
+ }
}
diff --git a/lib/imageResources.function.spec.ts b/lib/imageResources.function.spec.ts
new file mode 100644
index 00000000..a64600aa
--- /dev/null
+++ b/lib/imageResources.function.spec.ts
@@ -0,0 +1,28 @@
+import {loadImageResource} from "./imageResources.function";
+import {mockPartial} from "sneer";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {ImageReader} from "./provider";
+import {join} from "path";
+
+const loadMock = jest.fn();
+const providerRegistryMock = mockPartial({
+ getImageReader(): ImageReader {
+ return mockPartial({
+ load: loadMock
+ });
+ }
+});
+
+describe('imageResources', () => {
+ it('should retrieve an ImageReader via providerRegistry and load an image relative to the provided resourceDirectory', async () => {
+ // GIVEN
+ const resourceDirectoryPath = '/foo/bar';
+ const imageFileName = "image.png";
+
+ // WHEN
+ await loadImageResource(providerRegistryMock, resourceDirectoryPath, imageFileName);
+
+ // THEN
+ expect(loadMock).toBeCalledWith(join(resourceDirectoryPath, imageFileName));
+ });
+});
\ No newline at end of file
diff --git a/lib/imageResources.function.ts b/lib/imageResources.function.ts
new file mode 100644
index 00000000..96ce588e
--- /dev/null
+++ b/lib/imageResources.function.ts
@@ -0,0 +1,7 @@
+import {join, normalize} from "path";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+
+export function loadImageResource(providerRegistry: ProviderRegistry, resourceDirectory: string, fileName: string) {
+ const fullPath = normalize(join(resourceDirectory, fileName));
+ return providerRegistry.getImageReader().load(fullPath);
+}
\ No newline at end of file
diff --git a/lib/key.enum.ts b/lib/key.enum.ts
index fab519c0..f6f6ef0b 100644
--- a/lib/key.enum.ts
+++ b/lib/key.enum.ts
@@ -28,6 +28,18 @@ export enum Key {
F10,
F11,
F12,
+ F13,
+ F14,
+ F15,
+ F16,
+ F17,
+ F18,
+ F19,
+ F20,
+ F21,
+ F22,
+ F23,
+ F24,
Num0,
Num1,
@@ -116,4 +128,17 @@ export enum Key {
CapsLock,
ScrollLock,
NumLock,
+
+ AudioMute,
+ AudioVolDown,
+ AudioVolUp,
+ AudioPlay,
+ AudioStop,
+ AudioPause,
+ AudioPrev,
+ AudioNext,
+ AudioRewind,
+ AudioForward,
+ AudioRepeat,
+ AudioRandom
}
diff --git a/lib/keyboard.class.e2e.spec.ts b/lib/keyboard.class.e2e.spec.ts
deleted file mode 100644
index 0f9fed58..00000000
--- a/lib/keyboard.class.e2e.spec.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { jestMatchers, Key, keyboard, screen } from "../index";
-
-expect.extend(jestMatchers);
-
-const run = async (cmd: string) => {
- await keyboard.type(Key.LeftAlt, Key.F2);
- await keyboard.type(cmd);
- await keyboard.type(Key.Enter);
-};
-
-const confirm = async () => {
- await keyboard.type(Key.Enter);
-};
-
-const close = async () => {
- await keyboard.type(Key.LeftAlt, Key.F4);
-};
-
-describe("Keyboard e2e class", () => {
- it("should open gnome calculator via keyboard.", async () => {
- // GIVEN
- jest.setTimeout(30000);
- screen.config.resourceDirectory = "./e2e/assets";
- screen.config.confidence = 0.97;
- await run("gnome-calculator");
- await confirm();
-
- // WHEN
-
- // THEN
- await expect(screen).toShow("calculator.png");
- await close();
- });
-});
diff --git a/lib/keyboard.class.spec.ts b/lib/keyboard.class.spec.ts
index bd4e4e80..1406561e 100644
--- a/lib/keyboard.class.spec.ts
+++ b/lib/keyboard.class.spec.ts
@@ -1,114 +1,152 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-import { Key } from "./key.enum";
-import { Keyboard } from "./keyboard.class";
+import {Key} from "./key.enum";
+import {KeyboardClass} from "./keyboard.class";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {mockPartial} from "sneer";
+import {KeyboardProviderInterface} from "./provider";
-jest.mock("./adapter/native.adapter.class");
+jest.setTimeout(10000);
beforeEach(() => {
- jest.resetAllMocks();
+ jest.clearAllMocks();
});
-describe("Keyboard", () => {
- it("should have a default delay of 300 ms", () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
-
- // WHEN
-
- // THEN
- expect(SUT.config.autoDelayMs).toEqual(300);
- });
-
- it("should pass input strings down to the type call.", async () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = "Test input!";
-
- // WHEN
- await SUT.type(payload);
-
- // THEN
- expect(adapterMock.type).toHaveBeenCalledTimes(payload.length);
- for (const char of payload.split("")) {
- expect(adapterMock.type).toHaveBeenCalledWith(char);
- }
- });
-
- it("should pass multiple input strings down to the type call.", async () => {
- // GIVEN
- jest.setTimeout(10000);
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = ["Test input!", "Array test2"];
-
- // WHEN
- await SUT.type(...payload);
-
- // THEN
- expect(adapterMock.type).toHaveBeenCalledTimes(payload.join(" ").length);
- for (const char of payload.join(" ").split("")) {
- expect(adapterMock.type).toHaveBeenCalledWith(char);
- }
- });
-
- it("should pass input keys down to the click call.", async () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = [Key.A, Key.S, Key.D, Key.F];
-
- // WHEN
- await SUT.type(...payload);
-
- // THEN
- expect(adapterMock.click).toHaveBeenCalledTimes(1);
- expect(adapterMock.click).toHaveBeenCalledWith(...payload);
- });
-
- it("should pass a list of input keys down to the click call.", async () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = [Key.A, Key.S, Key.D, Key.F];
-
- // WHEN
- for (const key of payload) {
- await SUT.type(key);
- }
-
- // THEN
- expect(adapterMock.click).toHaveBeenCalledTimes(payload.length);
- });
-
- it("should pass a list of input keys down to the pressKey call.", async () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = [Key.A, Key.S, Key.D, Key.F];
-
- // WHEN
- for (const key of payload) {
- await SUT.pressKey(key);
- }
-
- // THEN
- expect(adapterMock.pressKey).toHaveBeenCalledTimes(payload.length);
- });
-
- it("should pass a list of input keys down to the releaseKey call.", async () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Keyboard(adapterMock);
- const payload = [Key.A, Key.S, Key.D, Key.F];
-
- // WHEN
- for (const key of payload) {
- await SUT.releaseKey(key);
+const providerRegistryMock = mockPartial({
+ getKeyboard(): KeyboardProviderInterface {
+ return mockPartial({
+ setKeyboardDelay: jest.fn(),
+ })
}
+})
- // THEN
- expect(adapterMock.releaseKey).toHaveBeenCalledTimes(payload.length);
- });
+describe("Keyboard", () => {
+ it("should have a default delay of 300 ms", () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+
+ // WHEN
+
+ // THEN
+ expect(SUT.config.autoDelayMs).toEqual(300);
+ });
+
+ it("should pass input strings down to the type call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = "Test input!";
+
+ const typeMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ type: typeMock
+ }));
+
+ // WHEN
+ await SUT.type(payload);
+
+ // THEN
+ expect(typeMock).toHaveBeenCalledTimes(payload.length);
+ for (const char of payload.split("")) {
+ expect(typeMock).toHaveBeenCalledWith(char);
+ }
+ });
+
+ it("should pass multiple input strings down to the type call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = ["Test input!", "Array test2"];
+
+ const typeMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ type: typeMock
+ }));
+
+ // WHEN
+ await SUT.type(...payload);
+
+ // THEN
+ expect(typeMock).toHaveBeenCalledTimes(payload.join(" ").length);
+ for (const char of payload.join(" ").split("")) {
+ expect(typeMock).toHaveBeenCalledWith(char);
+ }
+ });
+
+ it("should pass input keys down to the click call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = [Key.A, Key.S, Key.D, Key.F];
+
+ const clickMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ click: clickMock
+ }));
+
+ // WHEN
+ await SUT.type(...payload);
+
+ // THEN
+ expect(clickMock).toHaveBeenCalledTimes(1);
+ expect(clickMock).toHaveBeenCalledWith(...payload);
+ });
+
+ it("should pass a list of input keys down to the click call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = [Key.A, Key.S, Key.D, Key.F];
+
+ const clickMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ click: clickMock
+ }));
+
+ // WHEN
+ for (const key of payload) {
+ await SUT.type(key);
+ }
+
+ // THEN
+ expect(clickMock).toHaveBeenCalledTimes(payload.length);
+ });
+
+ it("should pass a list of input keys down to the pressKey call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = [Key.A, Key.S, Key.D, Key.F];
+
+ const keyMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ pressKey: keyMock
+ }));
+
+ // WHEN
+ for (const key of payload) {
+ await SUT.pressKey(key);
+ }
+
+ // THEN
+ expect(keyMock).toHaveBeenCalledTimes(payload.length);
+ });
+
+ it("should pass a list of input keys down to the releaseKey call.", async () => {
+ // GIVEN
+ const SUT = new KeyboardClass(providerRegistryMock);
+ const payload = [Key.A, Key.S, Key.D, Key.F];
+
+ const keyMock = jest.fn();
+ providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({
+ setKeyboardDelay: jest.fn(),
+ releaseKey: keyMock
+ }));
+
+ // WHEN
+ for (const key of payload) {
+ await SUT.releaseKey(key);
+ }
+
+ // THEN
+ expect(keyMock).toHaveBeenCalledTimes(payload.length);
+ });
});
diff --git a/lib/keyboard.class.ts b/lib/keyboard.class.ts
index 0a155c97..669e722b 100644
--- a/lib/keyboard.class.ts
+++ b/lib/keyboard.class.ts
@@ -1,105 +1,105 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-import { Key } from "./key.enum";
-import { sleep } from "./sleep.function";
+import {Key} from "./key.enum";
+import {sleep} from "./sleep.function";
+import {ProviderRegistry} from "./provider/provider-registry.class";
type StringOrKey = string[] | Key[];
-const inputIsString = (input: string[] | Key[]): input is string[] => {
- return input.every((elem: string | Key) => typeof elem === "string");
+const inputIsString = (input: (string | Key)[]): input is string[] => {
+ return input.every((elem: string | Key) => typeof elem === "string");
};
/**
- * {@link Keyboard} class provides methods to emulate keyboard input
+ * {@link KeyboardClass} class provides methods to emulate keyboard input
*/
-export class Keyboard {
+export class KeyboardClass {
- /**
- * Config object for {@link Keyboard} class
- */
- public config = {
/**
- * Configures the delay between single key events
+ * Config object for {@link KeyboardClass} class
*/
- autoDelayMs: 300,
- };
+ public config = {
+ /**
+ * Configures the delay between single key events
+ */
+ autoDelayMs: 300,
+ };
- /**
- * {@link Keyboard} class constructor
- * @param nativeAdapter {@link NativeAdapter} instance which bundles access to mouse, keyboard and clipboard
- */
- constructor(private nativeAdapter: NativeAdapter) {
- this.nativeAdapter.setKeyboardDelay(this.config.autoDelayMs);
- }
+ /**
+ * {@link KeyboardClass} class constructor
+ * @param providerRegistry
+ */
+ constructor(private providerRegistry: ProviderRegistry) {
+ this.providerRegistry.getKeyboard().setKeyboardDelay(this.config.autoDelayMs);
+ }
- /**
- * {@link type} types a sequence of {@link String} or single {@link Key}s via system keyboard
- * @example
- * ```typescript
- * await keyboard.type(Key.A, Key.S, Key.D, Key.F);
- * await keyboard.type("Hello, world!");
- * ```
- *
- * @param input Sequence of {@link String} or {@link Key} to type
- */
- public type(...input: StringOrKey): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- if (inputIsString(input)) {
- for (const char of input.join(" ").split("")) {
- await sleep(this.config.autoDelayMs);
- await this.nativeAdapter.type(char);
- }
- } else {
- await this.nativeAdapter.click(...input as Key[]);
- }
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link type} types a sequence of {@link String} or single {@link Key}s via system keyboard
+ * @example
+ * ```typescript
+ * await keyboard.type(Key.A, Key.S, Key.D, Key.F);
+ * await keyboard.type("Hello, world!");
+ * ```
+ *
+ * @param input Sequence of {@link String} or {@link Key} to type
+ */
+ public type(...input: StringOrKey): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ if (inputIsString(input)) {
+ for (const char of input.join(" ").split("")) {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getKeyboard().type(char);
+ }
+ } else {
+ await this.providerRegistry.getKeyboard().click(...input as Key[]);
+ }
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link pressKey} presses and holds a single {@link Key} for {@link Key} combinations
- * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press
- * @example
- * ```typescript
- * // Will press and hold key combination STRG + V
- * await keyboard.pressKey(Key.STRG, Key.A);
- * ```
- *
- * @param keys Array of {@link Key}s to press and hold
- */
- public pressKey(...keys: Key[]): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await this.nativeAdapter.pressKey(...keys);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link pressKey} presses and holds a single {@link Key} for {@link Key} combinations
+ * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press
+ * @example
+ * ```typescript
+ * // Will press and hold key combination STRG + V
+ * await keyboard.pressKey(Key.STRG, Key.A);
+ * ```
+ *
+ * @param keys Array of {@link Key}s to press and hold
+ */
+ public pressKey(...keys: Key[]): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.providerRegistry.getKeyboard().pressKey(...keys);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link pressKey} releases a single {@link Key} for {@link Key} combinations
- * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press
- * @example
- * ```typescript
- * // Will release key combination STRG + V
- * await keyboard.releaseKey(Key.STRG, Key.A);
- * ```
- *
- * @param keys Array of {@link Key}s to release
- */
- public releaseKey(...keys: Key[]): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await this.nativeAdapter.releaseKey(...keys);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link pressKey} releases a single {@link Key} for {@link Key} combinations
+ * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press
+ * @example
+ * ```typescript
+ * // Will release key combination STRG + V
+ * await keyboard.releaseKey(Key.STRG, Key.A);
+ * ```
+ *
+ * @param keys Array of {@link Key}s to release
+ */
+ public releaseKey(...keys: Key[]): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.providerRegistry.getKeyboard().releaseKey(...keys);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
}
diff --git a/lib/locationparameters.class.ts b/lib/locationparameters.class.ts
deleted file mode 100644
index 0febeb5f..00000000
--- a/lib/locationparameters.class.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {OptionalSearchParameters} from "./optionalsearchparameters.class";
-
-/**
- * {@deprecated This module serves as a polyfill to not break existing pre v1.5 code. Will be removed in v2.0.0}
- * Use {@link OptionalSearchParameters} instead
- */
-export {
- OptionalSearchParameters as LocationParameters
-}
\ No newline at end of file
diff --git a/lib/match-request.class.spec.ts b/lib/match-request.class.spec.ts
index df509638..d2eaaa76 100644
--- a/lib/match-request.class.spec.ts
+++ b/lib/match-request.class.spec.ts
@@ -1,21 +1,27 @@
-import { Image } from "./image.class";
-import { MatchRequest } from "./match-request.class";
-import { Region } from "./region.class";
+import {Image} from "./image.class";
+import {MatchRequest} from "./match-request.class";
+
+jest.mock('jimp', () => {});
describe("MatchRequest", () => {
- it("should default to multi-scale matching", () => {
- const SUT = new MatchRequest(
- new Image(100, 100,
- new ArrayBuffer(0), 3
- ),
- "foo",
- new Region(
- 0,
- 0,
- 100,
- 100),
- 0.99);
+ it("should default to multi-scale matching", () => {
+ const SUT = new MatchRequest(
+ new Image(
+ 100,
+ 100,
+ Buffer.from([]),
+ 3,
+ "haystack_image"
+ ),
+ new Image(
+ 100,
+ 100,
+ Buffer.from([]),
+ 3,
+ "needle_image"
+ ),
+ 0.99);
- expect(SUT.searchMultipleScales).toBeTruthy();
- });
+ expect(SUT.searchMultipleScales).toBeTruthy();
+ });
});
diff --git a/lib/match-request.class.ts b/lib/match-request.class.ts
index 78601102..d09d026a 100644
--- a/lib/match-request.class.ts
+++ b/lib/match-request.class.ts
@@ -1,11 +1,9 @@
import { Image } from "./image.class";
-import { Region } from "./region.class";
export class MatchRequest {
constructor(
public readonly haystack: Image,
- public readonly pathToNeedle: string,
- public readonly searchRegion: Region,
+ public readonly needle: Image,
public readonly confidence: number,
public readonly searchMultipleScales: boolean = true,
) {}
diff --git a/lib/mouse-movement.function.spec.ts b/lib/mouse-movement.function.spec.ts
new file mode 100644
index 00000000..abb5274a
--- /dev/null
+++ b/lib/mouse-movement.function.spec.ts
@@ -0,0 +1,80 @@
+import {
+ calculateStepDuration,
+ linear,
+ calculateMovementTimesteps, EasingFunction
+} from "./mouse-movement.function";
+
+describe("MovementType", () => {
+ describe("baseStepDuration", () => {
+ it("should calculate the base step duration in nanoseconds", () => {
+ // GIVEN
+ const speedInPixelsPerSecond = 1000;
+ const expectedBaseStepDuration = 1_000_000;
+
+ // WHEN
+ const result = calculateStepDuration(speedInPixelsPerSecond);
+
+ // THEN
+ expect(result).toBe(expectedBaseStepDuration);
+ });
+ });
+
+ describe("stepDuration", () => {
+ it("should call easing function progress to calculate current step duration", () => {
+ // GIVEN
+ const amountOfSteps = 100;
+ const speedInPixelsPerSecond = 1000;
+ const easingFunction = jest.fn(() => 0);
+
+ // WHEN
+ calculateMovementTimesteps(amountOfSteps, speedInPixelsPerSecond, easingFunction);
+
+ // THEN
+ expect(easingFunction).toBeCalledTimes(amountOfSteps);
+ })
+ });
+
+ describe('linear', () => {
+ it("should return a set of linear timesteps, 1000000 nanosecond per step.", () => {
+ // GIVEN
+ const expected = [1000000, 1000000, 1000000, 1000000, 1000000, 1000000];
+
+ // WHEN
+ const result = calculateMovementTimesteps(6, 1000, linear);
+
+ // THEN
+ expect(result).toEqual(expected);
+ });
+
+ it("should should return a set of linear timesteps, 2000000 nanoseconds per step.", () => {
+ // GIVEN
+ const expected = [2000000, 2000000, 2000000, 2000000, 2000000, 2000000];
+
+ // WHEN
+ const result = calculateMovementTimesteps(6, 500, linear);
+
+ // THEN
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('non-linear', () => {
+ it("should return progress slowly in the first half, 2000000 nanoseconds per step, then continue with normal speed, 1000000 nanoseconds per step", () => {
+ // GIVEN
+ const mouseSpeed = 1000;
+ const easingFunction: EasingFunction = (p: number) => {
+ if (p < 0.5) {
+ return -0.5 * mouseSpeed;
+ }
+ return 0;
+ };
+ const expected = [2000000, 2000000, 2000000, 1000000, 1000000, 1000000];
+
+ // WHEN
+ const result = calculateMovementTimesteps(6, mouseSpeed, easingFunction);
+
+ // THEN
+ expect(result).toEqual(expected);
+ });
+ });
+});
\ No newline at end of file
diff --git a/lib/mouse-movement.function.ts b/lib/mouse-movement.function.ts
new file mode 100644
index 00000000..3d435850
--- /dev/null
+++ b/lib/mouse-movement.function.ts
@@ -0,0 +1,31 @@
+/**
+ * {@link EasingFunction}s are used to modify movement behaviour.
+ *
+ * See https://easings.net/ for reference
+ */
+export interface EasingFunction {
+ (progressPercentage: number): number;
+}
+
+export const calculateStepDuration = (speedInPixelsPerSecond: number) => (1 / speedInPixelsPerSecond) * 1_000_000_000;
+
+export const calculateMovementTimesteps = (
+ amountOfSteps: number,
+ speedInPixelsPerSecond: number,
+ easingFunction: EasingFunction = linear
+): number[] => {
+ return Array(amountOfSteps)
+ .fill(speedInPixelsPerSecond)
+ .map((speed: number, idx: number) => {
+ let speedInPixels = speed;
+ if (typeof easingFunction === "function") {
+ speedInPixels += easingFunction(idx / amountOfSteps);
+ }
+ const stepDuration = calculateStepDuration(speedInPixels);
+ return (isFinite(stepDuration) && stepDuration > 0) ? stepDuration : 0;
+ });
+};
+
+export const linear: EasingFunction = (_: number): number => {
+ return 0;
+};
diff --git a/lib/mouse.class.spec.ts b/lib/mouse.class.spec.ts
index 8133507e..6302afb3 100644
--- a/lib/mouse.class.spec.ts
+++ b/lib/mouse.class.spec.ts
@@ -1,155 +1,217 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-import { Button } from "./button.enum";
-import { Mouse } from "./mouse.class";
-import { Point } from "./point.class";
-import { LineHelper } from "./util/linehelper.class";
-
-jest.mock("./adapter/native.adapter.class");
+import {Button} from "./button.enum";
+import {MouseClass} from "./mouse.class";
+import {Point} from "./point.class";
+import {LineHelper} from "./util/linehelper.class";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {mockPartial} from "sneer";
+import {MouseProviderInterface} from "./provider";
beforeEach(() => {
- jest.resetAllMocks();
+ jest.clearAllMocks();
});
const linehelper = new LineHelper();
+const providerRegistryMock = mockPartial({
+ getMouse(): MouseProviderInterface {
+ return mockPartial({
+ setMouseDelay: jest.fn()
+ })
+ }
+});
+
describe("Mouse class", () => {
- it("should have a default delay of 500 ms", () => {
- // GIVEN
- const adapterMock = new NativeAdapter();
- const SUT = new Mouse(adapterMock);
-
- // WHEN
-
- // THEN
- expect(SUT.config.autoDelayMs).toEqual(100);
- });
-
- it("should forward scrollLeft to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const scrollAmount = 5;
-
- // WHEN
- const result = await SUT.scrollLeft(scrollAmount);
-
- // THEN
- expect(nativeAdapterMock.scrollLeft).toBeCalledWith(scrollAmount);
- expect(result).toBe(SUT);
- });
-
- it("should forward scrollRight to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const scrollAmount = 5;
-
- // WHEN
- const result = await SUT.scrollRight(scrollAmount);
-
- // THEN
- expect(nativeAdapterMock.scrollRight).toBeCalledWith(scrollAmount);
- expect(result).toBe(SUT);
- });
-
- it("should forward scrollDown to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const scrollAmount = 5;
-
- // WHEN
- const result = await SUT.scrollDown(scrollAmount);
-
- // THEN
- expect(nativeAdapterMock.scrollDown).toBeCalledWith(scrollAmount);
- expect(result).toBe(SUT);
- });
-
- it("should forward scrollUp to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const scrollAmount = 5;
-
- // WHEN
- const result = await SUT.scrollUp(scrollAmount);
-
- // THEN
- expect(nativeAdapterMock.scrollUp).toBeCalledWith(scrollAmount);
- expect(result).toBe(SUT);
- });
-
- it("should forward leftClick to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
-
- // WHEN
- const result = await SUT.leftClick();
-
- // THEN
- expect(nativeAdapterMock.leftClick).toBeCalled();
- expect(result).toBe(SUT);
- });
-
- it("should forward rightClick to the native adapter class", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
-
- // WHEN
- const result = await SUT.rightClick();
-
- // THEN
- expect(nativeAdapterMock.rightClick).toBeCalled();
- expect(result).toBe(SUT);
- });
-
- it("update mouse position along path on move", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10));
-
- // WHEN
- const result = await SUT.move(path);
-
- // THEN
- expect(nativeAdapterMock.setMousePosition).toBeCalledTimes(path.length);
- expect(result).toBe(SUT);
- });
-
- it("should press and hold left mouse button, move and release left mouse button on drag", async () => {
- // GIVEN
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10));
-
- // WHEN
- const result = await SUT.drag(path);
-
- // THEN
- expect(nativeAdapterMock.pressButton).toBeCalledWith(Button.LEFT);
- expect(nativeAdapterMock.setMousePosition).toBeCalledTimes(path.length);
- expect(nativeAdapterMock.releaseButton).toBeCalledWith(Button.LEFT);
- expect(result).toBe(SUT);
- });
+ it("should have a default delay of 500 ms", () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+
+ // WHEN
+
+ // THEN
+ expect(SUT.config.autoDelayMs).toEqual(100);
+ });
+
+ it("should forward scrollLeft to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const scrollAmount = 5;
+
+ const scrollMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ scrollLeft: scrollMock
+ }));
+
+ // WHEN
+ const result = await SUT.scrollLeft(scrollAmount);
+
+ // THEN
+ expect(scrollMock).toBeCalledWith(scrollAmount);
+ expect(result).toBe(SUT);
+ });
+
+ it("should forward scrollRight to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const scrollAmount = 5;
+
+ const scrollMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ scrollRight: scrollMock
+ }));
+
+ // WHEN
+ const result = await SUT.scrollRight(scrollAmount);
+
+ // THEN
+ expect(scrollMock).toBeCalledWith(scrollAmount);
+ expect(result).toBe(SUT);
+ });
+
+ it("should forward scrollDown to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const scrollAmount = 5;
+
+ const scrollMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ scrollDown: scrollMock
+ }));
+
+ // WHEN
+ const result = await SUT.scrollDown(scrollAmount);
+
+ // THEN
+ expect(scrollMock).toBeCalledWith(scrollAmount);
+ expect(result).toBe(SUT);
+ });
+
+ it("should forward scrollUp to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const scrollAmount = 5;
+
+ const scrollMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ scrollUp: scrollMock
+ }));
+
+ // WHEN
+ const result = await SUT.scrollUp(scrollAmount);
+
+ // THEN
+ expect(scrollMock).toBeCalledWith(scrollAmount);
+ expect(result).toBe(SUT);
+ });
+
+ it("should forward leftClick to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+
+ const clickMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ leftClick: clickMock
+ }));
+
+ // WHEN
+ const result = await SUT.leftClick();
+
+ // THEN
+ expect(clickMock).toBeCalled();
+ expect(result).toBe(SUT);
+ });
+
+ it("should forward rightClick to the provider", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+
+ const clickMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ rightClick: clickMock
+ }));
+
+ // WHEN
+ const result = await SUT.rightClick();
+
+ // THEN
+ expect(clickMock).toBeCalled();
+ expect(result).toBe(SUT);
+ });
+
+ it("update mouse position along path on move", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10));
+
+ const setPositionMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ setMousePosition: setPositionMock
+ }));
+
+ // WHEN
+ const result = await SUT.move(path);
+
+ // THEN
+ expect(setPositionMock).toBeCalledTimes(path.length);
+ expect(result).toBe(SUT);
+ });
+
+ it("should press and hold left mouse button, move and release left mouse button on drag", async () => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10));
+
+ const setPositionMock = jest.fn();
+ const pressButtonMock = jest.fn();
+ const releaseButtonMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ setMousePosition: setPositionMock,
+ pressButton: pressButtonMock,
+ releaseButton: releaseButtonMock
+ }));
+
+ // WHEN
+ const result = await SUT.drag(path);
+
+ // THEN
+ expect(pressButtonMock).toBeCalledWith(Button.LEFT);
+ expect(setPositionMock).toBeCalledTimes(path.length);
+ expect(releaseButtonMock).toBeCalledWith(Button.LEFT);
+ expect(result).toBe(SUT);
+ });
});
describe("Mousebuttons", () => {
- it.each([
- [Button.LEFT, Button.LEFT],
- [Button.MIDDLE, Button.MIDDLE],
- [Button.RIGHT, Button.RIGHT],
- ] as Array<[Button, Button]>)("should be pressed and released", async (input: Button, expected: Button) => {
- const nativeAdapterMock = new NativeAdapter();
- const SUT = new Mouse(nativeAdapterMock);
- const pressed = await SUT.pressButton(input);
- const released = await SUT.releaseButton(input);
- expect(nativeAdapterMock.pressButton).toBeCalledWith(expected);
- expect(nativeAdapterMock.releaseButton).toBeCalledWith(expected);
- expect(pressed).toBe(SUT);
- expect(released).toBe(SUT);
- });
+ it.each([
+ [Button.LEFT, Button.LEFT],
+ [Button.MIDDLE, Button.MIDDLE],
+ [Button.RIGHT, Button.RIGHT],
+ ] as Array<[Button, Button]>)("should be pressed and released", async (input: Button, expected: Button) => {
+ // GIVEN
+ const SUT = new MouseClass(providerRegistryMock);
+ const pressButtonMock = jest.fn();
+ const releaseButtonMock = jest.fn();
+ providerRegistryMock.getMouse = jest.fn(() => mockPartial({
+ setMouseDelay: jest.fn(),
+ pressButton: pressButtonMock,
+ releaseButton: releaseButtonMock
+ }));
+
+ // WHEN
+ const pressed = await SUT.pressButton(input);
+ const released = await SUT.releaseButton(input);
+
+ // THEN
+ expect(pressButtonMock).toBeCalledWith(expected);
+ expect(releaseButtonMock).toBeCalledWith(expected);
+ expect(pressed).toBe(SUT);
+ expect(released).toBe(SUT);
+ });
});
diff --git a/lib/mouse.class.ts b/lib/mouse.class.ts
index f03e62cf..39c902ba 100644
--- a/lib/mouse.class.ts
+++ b/lib/mouse.class.ts
@@ -1,221 +1,221 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-import { Button } from "./button.enum";
-import { linear } from "./movementtype.function";
-import { Point } from "./point.class";
-import { busyWaitForNanoSeconds, sleep } from "./sleep.function";
+import {Button} from "./button.enum";
+import {Point} from "./point.class";
+import {busyWaitForNanoSeconds, sleep} from "./sleep.function";
+import {calculateMovementTimesteps, EasingFunction, linear} from "./mouse-movement.function";
+import {ProviderRegistry} from "./provider/provider-registry.class";
/**
- * {@link Mouse} class provides methods to emulate mouse input
+ * {@link MouseClass} class provides methods to emulate mouse input
*/
-export class Mouse {
- /**
- * Config object for {@link Mouse} class
- */
- public config = {
+export class MouseClass {
/**
- * Configures the delay between single mouse events
+ * Config object for {@link MouseClass} class
*/
- autoDelayMs: 100,
+ public config = {
+ /**
+ * Configures the delay between single mouse events
+ */
+ autoDelayMs: 100,
+
+ /**
+ * Configures the speed in pixels/second for mouse movement
+ */
+ mouseSpeed: 1000,
+ };
/**
- * Configures the speed in pixels/second for mouse movement
+ * {@link MouseClass} class constructor
+ * @param providerRegistry
*/
- mouseSpeed: 1000,
- };
-
- /**
- * {@link Mouse} class constructor
- * @param native {@link NativeAdapter} instance which bundles access to mouse, keyboard and clipboard
- */
- constructor(private native: NativeAdapter) {
- this.native.setMouseDelay(0);
- }
+ constructor(private providerRegistry: ProviderRegistry) {
+ this.providerRegistry.getMouse().setMouseDelay(0);
+ }
- /**
- * {@link setPosition} instantly moves the mouse cursor to a given {@link Point}
- * @param target {@link Point} to move the cursor to
- */
- public async setPosition(target: Point): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await this.native.setMousePosition(target);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link setPosition} instantly moves the mouse cursor to a given {@link Point}
+ * @param target {@link Point} to move the cursor to
+ */
+ public async setPosition(target: Point): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.providerRegistry.getMouse().setMousePosition(target);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link getPosition} returns a {@link Point} representing the current mouse position
- */
- public getPosition(): Promise {
- return this.native.currentMousePosition();
- }
+ /**
+ * {@link getPosition} returns a {@link Point} representing the current mouse position
+ */
+ public getPosition(): Promise {
+ return this.providerRegistry.getMouse().currentMousePosition();
+ }
- /**
- * {@link move} moves the mouse cursor along a given path of {@link Point}s, according to a movement type
- * @param path Array of {@link Point}s to follow
- * @param movementType Defines the type of mouse movement. Would allow to configured acceleration etc. (Default: {@link linear}, no acceleration)
- */
- public async move(path: Point[] | Promise, movementType = linear): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- const pathSteps = await path;
- const timeSteps = movementType(pathSteps.length, this.config.mouseSpeed);
- for (let idx = 0; idx < pathSteps.length; ++idx) {
- const node = pathSteps[idx];
- const minTime = timeSteps[idx];
- await busyWaitForNanoSeconds(minTime);
- await this.native.setMousePosition(node);
- }
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link move} moves the mouse cursor along a given path of {@link Point}s, according to a movement type
+ * @param path Array of {@link Point}s to follow
+ * @param movementType Defines the type of mouse movement. Would allow to configured acceleration etc. (Default: {@link linear}, no acceleration)
+ */
+ public async move(path: Point[] | Promise, movementType: EasingFunction = linear): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ const pathSteps = await path;
+ const timeSteps = calculateMovementTimesteps(pathSteps.length, this.config.mouseSpeed, movementType);
+ for (let idx = 0; idx < pathSteps.length; ++idx) {
+ const node = pathSteps[idx];
+ const minTime = timeSteps[idx];
+ await busyWaitForNanoSeconds(minTime);
+ await this.providerRegistry.getMouse().setMousePosition(node);
+ }
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link leftClick} performs a click with the left mouse button
- */
- public async leftClick(): Promise {
- return new Promise(async resolve => {
- await sleep(this.config.autoDelayMs);
- await this.native.leftClick();
- resolve(this);
- });
- }
+ /**
+ * {@link leftClick} performs a click with the left mouse button
+ */
+ public async leftClick(): Promise {
+ return new Promise(async resolve => {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().leftClick();
+ resolve(this);
+ });
+ }
- /**
- * {@link rightClick} performs a click with the right mouse button
- */
- public async rightClick(): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.rightClick();
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link rightClick} performs a click with the right mouse button
+ */
+ public async rightClick(): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().rightClick();
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link scrollDown} scrolls down for a given amount of "steps"
- * Please note that the actual scroll distance of a single "step" is OS dependent
- * @param amount The amount of "steps" to scroll
- */
- public async scrollDown(amount: number): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.scrollDown(amount);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link scrollDown} scrolls down for a given amount of "steps"
+ * Please note that the actual scroll distance of a single "step" is OS dependent
+ * @param amount The amount of "steps" to scroll
+ */
+ public async scrollDown(amount: number): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().scrollDown(amount);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link scrollUp} scrolls up for a given amount of "steps"
- * Please note that the actual scroll distance of a single "step" is OS dependent
- * @param amount The amount of "steps" to scroll
- */
- public async scrollUp(amount: number): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.scrollUp(amount);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link scrollUp} scrolls up for a given amount of "steps"
+ * Please note that the actual scroll distance of a single "step" is OS dependent
+ * @param amount The amount of "steps" to scroll
+ */
+ public async scrollUp(amount: number): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().scrollUp(amount);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link scrollLeft} scrolls left for a given amount of "steps"
- * Please note that the actual scroll distance of a single "step" is OS dependent
- * @param amount The amount of "steps" to scroll
- */
- public async scrollLeft(amount: number): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.scrollLeft(amount);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link scrollLeft} scrolls left for a given amount of "steps"
+ * Please note that the actual scroll distance of a single "step" is OS dependent
+ * @param amount The amount of "steps" to scroll
+ */
+ public async scrollLeft(amount: number): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().scrollLeft(amount);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link scrollRight} scrolls right for a given amount of "steps"
- * Please note that the actual scroll distance of a single "step" is OS dependent
- * @param amount The amount of "steps" to scroll
- */
- public async scrollRight(amount: number): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.scrollRight(amount);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link scrollRight} scrolls right for a given amount of "steps"
+ * Please note that the actual scroll distance of a single "step" is OS dependent
+ * @param amount The amount of "steps" to scroll
+ */
+ public async scrollRight(amount: number): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().scrollRight(amount);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link drag} drags the mouse along a certain path
- * In summary, {@link drag} presses and holds the left mouse button, moves the mouse and releases the left button
- * @param path The path of {@link Point}s to drag along
- */
- public async drag(path: Point[] | Promise): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await sleep(this.config.autoDelayMs);
- await this.native.pressButton(Button.LEFT);
- await this.move(path);
- await this.native.releaseButton(Button.LEFT);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link drag} drags the mouse along a certain path
+ * In summary, {@link drag} presses and holds the left mouse button, moves the mouse and releases the left button
+ * @param path The path of {@link Point}s to drag along
+ */
+ public async drag(path: Point[] | Promise): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await sleep(this.config.autoDelayMs);
+ await this.providerRegistry.getMouse().pressButton(Button.LEFT);
+ await this.move(path);
+ await this.providerRegistry.getMouse().releaseButton(Button.LEFT);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link pressButton} presses and holds a mouse button
- * @param btn The {@link Button} to press and hold
- */
- public async pressButton(btn: Button): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await this.native.pressButton(btn);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link pressButton} presses and holds a mouse button
+ * @param btn The {@link Button} to press and hold
+ */
+ public async pressButton(btn: Button): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.providerRegistry.getMouse().pressButton(btn);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- /**
- * {@link releaseButton} releases a mouse button previously pressed via {@link pressButton}
- * @param btn The {@link Button} to release
- */
- public async releaseButton(btn: Button): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- await this.native.releaseButton(btn);
- resolve(this);
- } catch (e) {
- reject(e);
- }
- });
- }
+ /**
+ * {@link releaseButton} releases a mouse button previously pressed via {@link pressButton}
+ * @param btn The {@link Button} to release
+ */
+ public async releaseButton(btn: Button): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ await this.providerRegistry.getMouse().releaseButton(btn);
+ resolve(this);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
}
diff --git a/lib/movement.function.ts b/lib/movement.function.ts
index 5c3feb1d..be8b2adf 100644
--- a/lib/movement.function.ts
+++ b/lib/movement.function.ts
@@ -1,30 +1,30 @@
-import { NativeAdapter } from "./adapter/native.adapter.class";
-import { MovementApi } from "./movement-api.interface";
-import { Point } from "./point.class";
-import { LineHelper } from "./util/linehelper.class";
+import {MovementApi} from "./movement-api.interface";
+import {Point} from "./point.class";
+import {LineHelper} from "./util/linehelper.class";
+import {ProviderRegistry} from "./provider/provider-registry.class";
-export const createMovementApi = (native: NativeAdapter, lineHelper: LineHelper): MovementApi => {
- return ({
- down: async (px: number): Promise => {
- const pos = await native.currentMousePosition();
- return lineHelper.straightLine(pos, new Point(pos.x, pos.y + px));
- },
- left: async (px: number): Promise => {
- const pos = await native.currentMousePosition();
- return lineHelper.straightLine(pos, new Point(pos.x - px, pos.y));
- },
- right: async (px: number): Promise => {
- const pos = await native.currentMousePosition();
- return lineHelper.straightLine(pos, new Point(pos.x + px, pos.y));
- },
- straightTo: async (target: Point | Promise): Promise => {
- const targetPoint = await target;
- const origin = await native.currentMousePosition();
- return lineHelper.straightLine(origin, targetPoint);
- },
- up: async (px: number): Promise => {
- const pos = await native.currentMousePosition();
- return lineHelper.straightLine(pos, new Point(pos.x, pos.y - px));
- },
- });
+export const createMovementApi = (providerRegistry: ProviderRegistry, lineHelper: LineHelper): MovementApi => {
+ return ({
+ down: async (px: number): Promise => {
+ const pos = await providerRegistry.getMouse().currentMousePosition();
+ return lineHelper.straightLine(pos, new Point(pos.x, pos.y + px));
+ },
+ left: async (px: number): Promise => {
+ const pos = await providerRegistry.getMouse().currentMousePosition();
+ return lineHelper.straightLine(pos, new Point(pos.x - px, pos.y));
+ },
+ right: async (px: number): Promise => {
+ const pos = await providerRegistry.getMouse().currentMousePosition();
+ return lineHelper.straightLine(pos, new Point(pos.x + px, pos.y));
+ },
+ straightTo: async (target: Point | Promise): Promise => {
+ const targetPoint = await target;
+ const origin = await providerRegistry.getMouse().currentMousePosition();
+ return lineHelper.straightLine(origin, targetPoint);
+ },
+ up: async (px: number): Promise => {
+ const pos = await providerRegistry.getMouse().currentMousePosition();
+ return lineHelper.straightLine(pos, new Point(pos.x, pos.y - px));
+ },
+ });
};
diff --git a/lib/movementtype.function.spec.ts b/lib/movementtype.function.spec.ts
deleted file mode 100644
index cf1220e1..00000000
--- a/lib/movementtype.function.spec.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { linear } from "./movementtype.function";
-
-describe("MovementType", () => {
- it("should return a set of linear timesteps, 1000000 nanosecond per step.", () => {
- const expected = [1000000, 1000000, 1000000, 1000000, 1000000, 1000000];
- expect(linear(6, 1000)).toEqual(expected);
- });
-
- it("should should return a set of linear timesteps, 2000000 nanoseconds per step.", () => {
- const expected = [2000000, 2000000, 2000000, 2000000, 2000000, 2000000];
- expect(linear(6, 500)).toEqual(expected);
- });
-});
diff --git a/lib/movementtype.function.ts b/lib/movementtype.function.ts
deleted file mode 100644
index dd99acb6..00000000
--- a/lib/movementtype.function.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export const linear = (
- amountOfSteps: number,
- speedInPixelsPerSecond: number,
-): number[] => {
- const timeSteps = [];
- // Duration per movement step in nanoseconds
- let stepDuration = (1 / speedInPixelsPerSecond) * 1_000_000_000;
- if (stepDuration <= 0) {
- stepDuration = 0;
- }
- for (let idx = 0; idx < amountOfSteps; ++idx) {
- timeSteps.push(stepDuration);
- }
- return timeSteps;
-};
diff --git a/lib/provider/native/clipboard-action-provider.interface.ts b/lib/provider/clipboard-provider.interface.ts
similarity index 94%
rename from lib/provider/native/clipboard-action-provider.interface.ts
rename to lib/provider/clipboard-provider.interface.ts
index f451b669..17c6637a 100644
--- a/lib/provider/native/clipboard-action-provider.interface.ts
+++ b/lib/provider/clipboard-provider.interface.ts
@@ -1,7 +1,7 @@
/**
* A ClipboardActionProvider should allow access to the system clipboard
*/
-export interface ClipboardActionProvider {
+export interface ClipboardProviderInterface {
/**
* hasText should return whether the system clipboard currently holds text or not
*
diff --git a/lib/provider/data-sink.interface.ts b/lib/provider/data-sink.interface.ts
new file mode 100644
index 00000000..47a0d0e8
--- /dev/null
+++ b/lib/provider/data-sink.interface.ts
@@ -0,0 +1,12 @@
+/**
+ * A DataSink should provide methods to store data
+ *
+ * @interface DataSinkInterface
+ */
+export interface DataSinkInterface {
+ /**
+ * store will store data to disk
+ * @param parameters Required parameters
+ */
+ store(parameters: PARAMETER_TYPE): Promise;
+}
diff --git a/lib/provider/data-source.interface.ts b/lib/provider/data-source.interface.ts
new file mode 100644
index 00000000..e2d57f7b
--- /dev/null
+++ b/lib/provider/data-source.interface.ts
@@ -0,0 +1,12 @@
+/**
+ * A DataSource should provide methods to load data
+ *
+ * @interface DataSourceInterface
+ */
+export interface DataSourceInterface {
+ /**
+ * load will load data from disk
+ * @param parameters Required parameters
+ */
+ load(parameters: PARAMETER_TYPE): Promise;
+}
diff --git a/lib/provider/image-finder.interface.ts b/lib/provider/image-finder.interface.ts
new file mode 100644
index 00000000..07bb496b
--- /dev/null
+++ b/lib/provider/image-finder.interface.ts
@@ -0,0 +1,29 @@
+import { MatchRequest } from "../match-request.class";
+import { MatchResult } from "../match-result.class";
+
+/**
+ * An ImageFinder should provide an abstraction layer to perform image matching
+ *
+ * @interface ImageFinderInterface
+ */
+export interface ImageFinderInterface {
+ /**
+ * findMatch should provide an abstraction to search for an image needle
+ * in another image haystack
+ *
+ * @param {MatchRequest} matchRequest A {@link MatchRequest} containing needed matching data
+ * @returns {Promise} A {@link MatchResult} holding the match probability and location
+ * @memberof ImageFinderInterface
+ */
+ findMatch(matchRequest: MatchRequest): Promise;
+
+ /**
+ * findMatches should provide an abstraction to search for an image needle
+ * in another image haystack
+ *
+ * @param {MatchRequest} matchRequest A matchrequest containing needed matching data
+ * @returns {Promise} A list of {@link MatchResult}s holding the match probability and location
+ * @memberof ImageFinderInterface
+ */
+ findMatches(matchRequest: MatchRequest): Promise;
+}
diff --git a/lib/provider/image-processor.interface.ts b/lib/provider/image-processor.interface.ts
new file mode 100644
index 00000000..1eca6ffb
--- /dev/null
+++ b/lib/provider/image-processor.interface.ts
@@ -0,0 +1,19 @@
+import {Point} from "../point.class";
+import {RGBA} from "../rgba.class";
+import {Image} from "../image.class";
+
+/**
+ * An ImageProcessor should provide an abstraction layer to perform
+ * image processing via a 3rd part library
+ *
+ * @interface ImageFinderInterface
+ */
+export interface ImageProcessor {
+
+ /**
+ * {@link colorAt} returns a pixels {@link RGBA} value
+ * @param image The {@link Image} to query color information from
+ * @param location The {@link Point} where to query color information
+ */
+ colorAt(image: Image | Promise, location: Point | Promise): Promise;
+}
\ No newline at end of file
diff --git a/lib/provider/image-reader.type.ts b/lib/provider/image-reader.type.ts
new file mode 100644
index 00000000..04b8c4f1
--- /dev/null
+++ b/lib/provider/image-reader.type.ts
@@ -0,0 +1,4 @@
+import {DataSourceInterface} from "./data-source.interface";
+import {Image} from "../image.class";
+
+export type ImageReader = DataSourceInterface;
diff --git a/lib/provider/image-writer.type.ts b/lib/provider/image-writer.type.ts
new file mode 100644
index 00000000..0d063fee
--- /dev/null
+++ b/lib/provider/image-writer.type.ts
@@ -0,0 +1,9 @@
+import {Image} from "../image.class";
+import {DataSinkInterface} from "./data-sink.interface";
+
+export interface ImageWriterParameters {
+ image: Image,
+ path: string
+}
+
+export type ImageWriter = DataSinkInterface;
diff --git a/lib/provider/image/jimp-image-processor.class.ts b/lib/provider/image/jimp-image-processor.class.ts
new file mode 100644
index 00000000..832f71fe
--- /dev/null
+++ b/lib/provider/image/jimp-image-processor.class.ts
@@ -0,0 +1,24 @@
+import Jimp from 'jimp';
+import {Image} from "../../image.class";
+import {Point} from "../../point.class";
+import {ImageProcessor} from "../image-processor.interface";
+import {imageToJimp} from "../io/imageToJimp.function";
+import {RGBA} from "../../rgba.class";
+
+export default class implements ImageProcessor {
+ colorAt(image: Image | Promise, point: Point | Promise): Promise {
+ return new Promise(async (resolve, reject) => {
+ const location = await point;
+ const img = await image;
+ if (location.x < 0 || location.x >= img.width) {
+ reject(`Query location out of bounds. Should be in range 0 <= x < image.width, is ${location.x}`);
+ }
+ if (location.y < 0 || location.y >= img.height) {
+ reject(`Query location out of bounds. Should be in range 0 <= y < image.height, is ${location.y}`);
+ }
+ const jimpImage = imageToJimp(img);
+ const rgba = Jimp.intToRGBA(jimpImage.getPixelColor(location.x, location.y));
+ resolve(new RGBA(rgba.r, rgba.g, rgba.b, rgba.a));
+ });
+ }
+}
diff --git a/lib/provider/index.ts b/lib/provider/index.ts
new file mode 100644
index 00000000..f20d3799
--- /dev/null
+++ b/lib/provider/index.ts
@@ -0,0 +1,10 @@
+export {ClipboardProviderInterface} from "./clipboard-provider.interface";
+export {DataSinkInterface} from "./data-sink.interface";
+export {DataSourceInterface} from "./data-source.interface";
+export {ImageFinderInterface} from "./image-finder.interface";
+export {ImageReader} from "./image-reader.type";
+export {ImageWriter, ImageWriterParameters} from "./image-writer.type";
+export {KeyboardProviderInterface} from "./keyboard-provider.interface";
+export {MouseProviderInterface} from "./mouse-provider.interface";
+export {ScreenProviderInterface} from "./screen-provider.interface";
+export {WindowProviderInterface} from "./window-provider.interface";
\ No newline at end of file
diff --git a/lib/provider/io/__mocks__/calculator.png b/lib/provider/io/__mocks__/calculator.png
new file mode 100644
index 00000000..8f6cf3cb
Binary files /dev/null and b/lib/provider/io/__mocks__/calculator.png differ
diff --git a/lib/provider/io/imageToJimp.function.spec.ts b/lib/provider/io/imageToJimp.function.spec.ts
new file mode 100644
index 00000000..30c527c1
--- /dev/null
+++ b/lib/provider/io/imageToJimp.function.spec.ts
@@ -0,0 +1,38 @@
+import {Image} from "../../image.class";
+import {imageToJimp} from "./imageToJimp.function";
+import Jimp from "jimp";
+
+jest.mock('jimp', () => {
+ class JimpMock {
+ bitmap = {
+ width: 100,
+ height: 100,
+ data: Buffer.from([]),
+ }
+ hasAlpha = () => false
+ static read = jest.fn(() => Promise.resolve(new JimpMock()))
+ }
+
+ return ({
+ __esModule: true,
+ default: JimpMock
+ })
+});
+
+afterEach(() => jest.resetAllMocks());
+
+describe('imageToJimp', () => {
+ it('should successfully convert an Image to a Jimp instance', async () => {
+ // GIVEN
+ const scanMock = jest.fn();
+ Jimp.prototype.scan = scanMock;
+ const inputImage = new Image(1, 1, Buffer.from([0, 0, 0]),3, "input_image");
+
+ // WHEN
+ const result = await imageToJimp(inputImage);
+
+ // THEN
+ expect(result).toBeInstanceOf(Jimp);
+ expect(scanMock).toHaveBeenCalledTimes(1);
+ });
+});
\ No newline at end of file
diff --git a/lib/provider/io/imageToJimp.function.ts b/lib/provider/io/imageToJimp.function.ts
new file mode 100644
index 00000000..b91cdff4
--- /dev/null
+++ b/lib/provider/io/imageToJimp.function.ts
@@ -0,0 +1,20 @@
+import Jimp from "jimp";
+import {Image} from "../../image.class";
+import {ColorMode} from "../../colormode.enum";
+
+export function imageToJimp(image: Image): Jimp {
+ const jimpImage = new Jimp({
+ data: image.data,
+ width: image.width,
+ height: image.height
+ });
+ if (image.colorMode === ColorMode.BGR) {
+ // Image treats data in BGR format, so we have to switch red and blue color channels
+ jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
+ const red = this.bitmap.data[idx];
+ this.bitmap.data[idx] = this.bitmap.data[idx + 2];
+ this.bitmap.data[idx + 2] = red;
+ });
+ }
+ return jimpImage;
+}
\ No newline at end of file
diff --git a/lib/provider/io/jimp-image-reader.class.spec.ts b/lib/provider/io/jimp-image-reader.class.spec.ts
new file mode 100644
index 00000000..837b6d5c
--- /dev/null
+++ b/lib/provider/io/jimp-image-reader.class.spec.ts
@@ -0,0 +1,58 @@
+import ImageReader from "./jimp-image-reader.class";
+import {join} from "path";
+import Jimp from "jimp";
+
+jest.mock('jimp', () => {
+ class JimpMock {
+ bitmap = {
+ width: 100,
+ height: 100,
+ data: Buffer.from([]),
+ }
+ hasAlpha = () => false
+ static read = jest.fn(() => Promise.resolve(new JimpMock()))
+ }
+
+ return ({
+ __esModule: true,
+ default: JimpMock
+ })
+});
+
+afterEach(() => jest.resetAllMocks());
+
+describe('Jimp image reader', () => {
+ it('should return an Image object', async () => {
+ // GIVEN
+ const inputPath = join(__dirname, "__mocks__", "calculator.png");
+ const scanMock = jest.fn();
+ Jimp.prototype.scan = scanMock;
+ const SUT = new ImageReader();
+
+ // WHEN
+ await SUT.load(inputPath);
+
+ // THEN
+ expect(scanMock).toHaveBeenCalledTimes(1);
+ expect(Jimp.read).toBeCalledTimes(1);
+ expect(Jimp.read).toBeCalledWith(inputPath);
+ });
+
+ it('should reject on loading failures', async () => {
+ // GIVEN
+ const inputPath = "/some/path/to/file";
+ const expectedError = "Error during load";
+ const SUT = new ImageReader();
+ Jimp.read = jest.fn(() => {
+ throw new Error(expectedError);
+ })
+
+ // WHEN
+ try {
+ await SUT.load(inputPath);
+ } catch (err) {
+ // THEN
+ expect(err).toStrictEqual(Error(expectedError));
+ }
+ });
+});
\ No newline at end of file
diff --git a/lib/provider/io/jimp-image-reader.class.ts b/lib/provider/io/jimp-image-reader.class.ts
new file mode 100644
index 00000000..f33089be
--- /dev/null
+++ b/lib/provider/io/jimp-image-reader.class.ts
@@ -0,0 +1,28 @@
+import Jimp from 'jimp';
+import {ImageReader} from "../image-reader.type";
+import {Image} from "../../image.class";
+import {ColorMode} from "../../colormode.enum";
+
+export default class implements ImageReader {
+ load(parameters: string): Promise {
+ return new Promise((resolve, reject) => {
+ Jimp.read(parameters)
+ .then(jimpImage => {
+ // stay consistent with images retrieved from libnut which uses BGR format
+ jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
+ const red = this.bitmap.data[idx];
+ this.bitmap.data[idx] = this.bitmap.data[idx + 2];
+ this.bitmap.data[idx + 2] = red;
+ });
+ resolve(new Image(
+ jimpImage.bitmap.width,
+ jimpImage.bitmap.height,
+ jimpImage.bitmap.data,
+ jimpImage.hasAlpha() ? 4 : 3,
+ parameters,
+ ColorMode.BGR
+ ));
+ }).catch(err => reject(`Failed to load image from '${parameters}'. Reason: ${err}`));
+ })
+ }
+}
diff --git a/lib/provider/io/jimp-image-writer.class.spec.ts b/lib/provider/io/jimp-image-writer.class.spec.ts
new file mode 100644
index 00000000..480cae06
--- /dev/null
+++ b/lib/provider/io/jimp-image-writer.class.spec.ts
@@ -0,0 +1,43 @@
+import ImageWriter from "./jimp-image-writer.class";
+import {Image} from "../../image.class";
+import Jimp from "jimp";
+
+jest.mock('jimp', () => {
+ class JimpMock {
+ bitmap = {
+ width: 100,
+ height: 100,
+ data: Buffer.from([]),
+ }
+ hasAlpha = () => false
+ static read = jest.fn(() => Promise.resolve(new JimpMock()))
+ }
+
+ return ({
+ __esModule: true,
+ default: JimpMock
+ })
+});
+
+afterEach(() => jest.resetAllMocks());
+
+describe('Jimp image writer', () => {
+ it('should reject on writing failures', async () => {
+ // GIVEN
+ const outputFileName = "/does/not/compute.png"
+ const outputFile = new Image(100, 200, Buffer.from([]), 3, outputFileName);
+ const writeMock = jest.fn(() => Promise.resolve(new Jimp()));
+ const scanMock = jest.fn();
+ Jimp.prototype.scan = scanMock;
+ Jimp.prototype.writeAsync = writeMock;
+ const SUT = new ImageWriter();
+
+ // WHEN
+ await SUT.store({image: outputFile, path: outputFileName});
+
+ // THEN
+ expect(scanMock).toHaveBeenCalledTimes(1)
+ expect(writeMock).toHaveBeenCalledTimes(1)
+ expect(writeMock).toHaveBeenCalledWith(outputFileName)
+ });
+});
\ No newline at end of file
diff --git a/lib/provider/io/jimp-image-writer.class.ts b/lib/provider/io/jimp-image-writer.class.ts
new file mode 100644
index 00000000..664b7794
--- /dev/null
+++ b/lib/provider/io/jimp-image-writer.class.ts
@@ -0,0 +1,14 @@
+import {ImageWriter, ImageWriterParameters} from "../image-writer.type";
+import {imageToJimp} from "./imageToJimp.function";
+
+export default class implements ImageWriter {
+ store(parameters: ImageWriterParameters): Promise {
+ return new Promise((resolve, reject) => {
+ const jimpImage = imageToJimp(parameters.image);
+ jimpImage
+ .writeAsync(parameters.path)
+ .then(_ => resolve())
+ .catch(err => reject(err));
+ });
+ }
+}
diff --git a/lib/provider/native/keyboard-action-provider.interface.ts b/lib/provider/keyboard-provider.interface.ts
similarity index 92%
rename from lib/provider/native/keyboard-action-provider.interface.ts
rename to lib/provider/keyboard-provider.interface.ts
index 6cedae6f..ea4305b9 100644
--- a/lib/provider/native/keyboard-action-provider.interface.ts
+++ b/lib/provider/keyboard-provider.interface.ts
@@ -1,9 +1,9 @@
-import { Key } from "../../key.enum";
+import { Key } from "../key.enum";
/**
* A KeyboardActionProvider should provide access to a systems keyboard
*/
-export interface KeyboardActionProvider {
+export interface KeyboardProviderInterface {
/**
* setKeyboardDelay should allow to configure a delay between key presses
*
diff --git a/lib/provider/native/mouse-action-provider.interface.ts b/lib/provider/mouse-provider.interface.ts
similarity index 93%
rename from lib/provider/native/mouse-action-provider.interface.ts
rename to lib/provider/mouse-provider.interface.ts
index 7240dbce..bc58d6dd 100644
--- a/lib/provider/native/mouse-action-provider.interface.ts
+++ b/lib/provider/mouse-provider.interface.ts
@@ -1,10 +1,10 @@
-import { Button } from "../../button.enum";
-import { Point } from "../../point.class";
+import { Button } from "../button.enum";
+import { Point } from "../point.class";
/**
* A MouseActionProvider should provide access to a systems mouse input
*/
-export interface MouseActionProvider {
+export interface MouseProviderInterface {
/**
* setMouseDelay should allow to configure mouse movement speed
*
diff --git a/lib/provider/native/clipboardy-clipboard-action.class.spec.ts b/lib/provider/native/clipboardy-clipboard.class.spec.ts
similarity index 71%
rename from lib/provider/native/clipboardy-clipboard-action.class.spec.ts
rename to lib/provider/native/clipboardy-clipboard.class.spec.ts
index ff368d51..9e96a795 100644
--- a/lib/provider/native/clipboardy-clipboard-action.class.spec.ts
+++ b/lib/provider/native/clipboardy-clipboard.class.spec.ts
@@ -1,4 +1,6 @@
-import ClipboardAction from "./clipboardy-clipboard-action.class";
+import ClipboardAction from "./clipboardy-clipboard.class";
+
+jest.mock('jimp', () => {});
beforeEach(() => {
jest.resetAllMocks();
@@ -6,7 +8,7 @@ beforeEach(() => {
describe("clipboardy action", () => {
describe("copy", () => {
- it("should resolve", async done => {
+ it("should resolve", async () => {
// GIVEN
const SUT = new ClipboardAction();
const testText = "test";
@@ -15,11 +17,10 @@ describe("clipboardy action", () => {
// THEN
await SUT.copy(testText);
- done();
});
});
describe("hasText", () => {
- it("should return true when text has been copied", async done => {
+ it("should return true when text has been copied", async () => {
// GIVEN
const SUT = new ClipboardAction();
const testText = "test";
@@ -29,7 +30,6 @@ describe("clipboardy action", () => {
// THEN
await expect(SUT.hasText()).resolves.toBeTruthy();
- done();
});
});
});
diff --git a/lib/provider/native/clipboardy-clipboard-action.class.ts b/lib/provider/native/clipboardy-clipboard.class.ts
similarity index 83%
rename from lib/provider/native/clipboardy-clipboard-action.class.ts
rename to lib/provider/native/clipboardy-clipboard.class.ts
index 03ee2f18..a8ab2747 100644
--- a/lib/provider/native/clipboardy-clipboard-action.class.ts
+++ b/lib/provider/native/clipboardy-clipboard.class.ts
@@ -1,7 +1,7 @@
import clippy from "clipboardy";
-import { ClipboardActionProvider } from "./clipboard-action-provider.interface";
+import { ClipboardProviderInterface } from "../clipboard-provider.interface";
-export default class implements ClipboardActionProvider {
+export default class implements ClipboardProviderInterface {
constructor() {
}
diff --git a/lib/provider/native/libnut-keyboard-action.class.ts b/lib/provider/native/libnut-keyboard-action.class.ts
deleted file mode 100644
index a71cef5f..00000000
--- a/lib/provider/native/libnut-keyboard-action.class.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import libnut = require("@nut-tree/libnut");
-import { Key } from "../../key.enum";
-import { KeyboardActionProvider } from "./keyboard-action-provider.interface";
-
-export default class KeyboardAction implements KeyboardActionProvider {
-
- public static KeyLookupMap = new Map([
- [Key.A, "a"],
- [Key.B, "b"],
- [Key.C, "c"],
- [Key.D, "d"],
- [Key.E, "e"],
- [Key.F, "f"],
- [Key.G, "g"],
- [Key.H, "h"],
- [Key.I, "i"],
- [Key.J, "j"],
- [Key.K, "k"],
- [Key.L, "l"],
- [Key.M, "m"],
- [Key.N, "n"],
- [Key.O, "o"],
- [Key.P, "p"],
- [Key.Q, "q"],
- [Key.R, "r"],
- [Key.S, "s"],
- [Key.T, "t"],
- [Key.U, "u"],
- [Key.V, "v"],
- [Key.W, "w"],
- [Key.X, "x"],
- [Key.Y, "y"],
- [Key.Z, "z"],
-
- [Key.F1, "f1"],
- [Key.F2, "f2"],
- [Key.F3, "f3"],
- [Key.F4, "f4"],
- [Key.F5, "f5"],
- [Key.F6, "f6"],
- [Key.F7, "f7"],
- [Key.F8, "f8"],
- [Key.F9, "f9"],
- [Key.F10, "f10"],
- [Key.F11, "f11"],
- [Key.F12, "f12"],
-
- [Key.Num0, "0"],
- [Key.Num1, "1"],
- [Key.Num2, "2"],
- [Key.Num3, "3"],
- [Key.Num4, "4"],
- [Key.Num5, "5"],
- [Key.Num6, "6"],
- [Key.Num7, "7"],
- [Key.Num8, "8"],
- [Key.Num9, "9"],
- [Key.NumPad0, "numpad_0"],
- [Key.NumPad1, "numpad_1"],
- [Key.NumPad2, "numpad_2"],
- [Key.NumPad3, "numpad_3"],
- [Key.NumPad4, "numpad_4"],
- [Key.NumPad5, "numpad_5"],
- [Key.NumPad6, "numpad_6"],
- [Key.NumPad7, "numpad_7"],
- [Key.NumPad8, "numpad_8"],
- [Key.NumPad9, "numpad_9"],
-
- [Key.Space, "space"],
- [Key.Escape, "escape"],
- [Key.Tab, "tab"],
- [Key.LeftAlt, "alt"],
- [Key.LeftControl, "control"],
- [Key.RightAlt, "alt"],
- [Key.RightControl, "control"],
-
- [Key.LeftShift, "shift"],
- [Key.LeftSuper, "command"],
- [Key.RightShift, "space"],
- [Key.RightSuper, "command"],
-
- [Key.Grave, "~"],
- [Key.Minus, "-"],
- [Key.Equal, "="],
- [Key.Backspace, "backspace"],
- [Key.LeftBracket, "["],
- [Key.RightBracket, "]"],
- [Key.Backslash, "\\"],
- [Key.Semicolon, ";"],
- [Key.Quote, "'"],
- [Key.Return, "enter"],
- [Key.Comma, ","],
- [Key.Period, "."],
- [Key.Slash, "/"],
-
- [Key.Left, "left"],
- [Key.Up, "up"],
- [Key.Right, "right"],
- [Key.Down, "down"],
-
- [Key.Print, "printscreen"],
- [Key.Pause, null],
- [Key.Insert, "insert"],
- [Key.Delete, null],
- [Key.Home, "home"],
- [Key.End, "end"],
- [Key.PageUp, "pageup"],
- [Key.PageDown, "pagedown"],
-
- [Key.Add, null],
- [Key.Subtract, null],
- [Key.Multiply, null],
- [Key.Divide, null],
- [Key.Decimal, null],
- [Key.Enter, "enter"],
-
- [Key.CapsLock, null],
- [Key.ScrollLock, null],
- [Key.NumLock, null],
- ]);
-
- public static keyLookup(key: Key): any {
- return this.KeyLookupMap.get(key);
- }
-
- private static mapModifierKeys(...keys: Key[]): string[] {
- return keys
- .map(modifier => KeyboardAction.keyLookup(modifier))
- .filter(modifierKey => modifierKey != null && modifierKey.length > 1);
- }
-
- private static key(key: Key, event: "up" | "down", ...modifiers: Key[]): Promise {
- return new Promise((resolve, reject) => {
- try {
- const nativeKey = KeyboardAction.keyLookup(key);
- const modifierKeys = this.mapModifierKeys(...modifiers);
- if (nativeKey) {
- libnut.keyToggle(nativeKey, event, modifierKeys);
- }
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
-
- constructor() {
- }
-
- public type(input: string): Promise {
- return new Promise((resolve, reject) => {
- try {
- libnut.typeString(input);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
-
- public click(...keys: Key[]): Promise {
- return new Promise((resolve, reject) => {
- try {
- keys.reverse();
- const [key, ...modifiers] = keys;
- const nativeKey = KeyboardAction.keyLookup(key);
- const modifierKeys = KeyboardAction.mapModifierKeys(...modifiers);
- if (nativeKey) {
- libnut.keyTap(nativeKey, modifierKeys);
- }
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
-
- public pressKey(...keys: Key[]): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- keys.reverse();
- const [key, ...modifiers] = keys;
- await KeyboardAction.key(key, "down", ...modifiers);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
-
- public releaseKey(...keys: Key[]): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- keys.reverse();
- const [key, ...modifiers] = keys;
- await KeyboardAction.key(key, "up", ...modifiers);
- resolve();
- } catch (e) {
- reject(e);
- }
- });
- }
-
- public setKeyboardDelay(delay: number): void {
- libnut.setKeyboardDelay(delay);
- }
-}
diff --git a/lib/provider/native/libnut-keyboard.action.class.spec.ts b/lib/provider/native/libnut-keyboard.class.spec.ts
similarity index 85%
rename from lib/provider/native/libnut-keyboard.action.class.spec.ts
rename to lib/provider/native/libnut-keyboard.class.spec.ts
index f4f1e124..d1cedfcf 100644
--- a/lib/provider/native/libnut-keyboard.action.class.spec.ts
+++ b/lib/provider/native/libnut-keyboard.class.spec.ts
@@ -1,6 +1,6 @@
import libnut = require("@nut-tree/libnut");
import { Key } from "../../key.enum";
-import KeyboardAction from "./libnut-keyboard-action.class";
+import KeyboardAction from "./libnut-keyboard.class";
jest.mock("@nut-tree/libnut");
@@ -175,4 +175,30 @@ describe("libnut keyboard action", () => {
expect(SUT.releaseKey(Key.A)).rejects.toThrowError("Test error");
});
});
+
+ describe("bugfix #260", () => {
+ it("should forward the pressKey call to libnut for 'delete'", () => {
+ // GIVEN
+ const SUT = new KeyboardAction();
+
+ // WHEN
+ SUT.pressKey(Key.Delete);
+
+ // THEN
+ expect(libnut.keyToggle).toBeCalledTimes(1);
+ expect(libnut.keyToggle).toBeCalledWith("delete", "down", []);
+ });
+
+ it("should forward the releaseKey call to libnut for 'delete'", () => {
+ // GIVEN
+ const SUT = new KeyboardAction();
+
+ // WHEN
+ SUT.releaseKey(Key.Delete);
+
+ // THEN
+ expect(libnut.keyToggle).toBeCalledTimes(1);
+ expect(libnut.keyToggle).toBeCalledWith("delete", "up", []);
+ });
+ });
});
diff --git a/lib/provider/native/libnut-keyboard.class.ts b/lib/provider/native/libnut-keyboard.class.ts
new file mode 100644
index 00000000..60846bc1
--- /dev/null
+++ b/lib/provider/native/libnut-keyboard.class.ts
@@ -0,0 +1,232 @@
+import libnut = require("@nut-tree/libnut");
+import {Key} from "../../key.enum";
+import {KeyboardProviderInterface} from "../keyboard-provider.interface";
+
+export default class KeyboardAction implements KeyboardProviderInterface {
+
+ public static KeyLookupMap = new Map([
+ [Key.A, "a"],
+ [Key.B, "b"],
+ [Key.C, "c"],
+ [Key.D, "d"],
+ [Key.E, "e"],
+ [Key.F, "f"],
+ [Key.G, "g"],
+ [Key.H, "h"],
+ [Key.I, "i"],
+ [Key.J, "j"],
+ [Key.K, "k"],
+ [Key.L, "l"],
+ [Key.M, "m"],
+ [Key.N, "n"],
+ [Key.O, "o"],
+ [Key.P, "p"],
+ [Key.Q, "q"],
+ [Key.R, "r"],
+ [Key.S, "s"],
+ [Key.T, "t"],
+ [Key.U, "u"],
+ [Key.V, "v"],
+ [Key.W, "w"],
+ [Key.X, "x"],
+ [Key.Y, "y"],
+ [Key.Z, "z"],
+
+ [Key.F1, "f1"],
+ [Key.F2, "f2"],
+ [Key.F3, "f3"],
+ [Key.F4, "f4"],
+ [Key.F5, "f5"],
+ [Key.F6, "f6"],
+ [Key.F7, "f7"],
+ [Key.F8, "f8"],
+ [Key.F9, "f9"],
+ [Key.F10, "f10"],
+ [Key.F11, "f11"],
+ [Key.F12, "f12"],
+ [Key.F13, "f13"],
+ [Key.F14, "f14"],
+ [Key.F15, "f15"],
+ [Key.F16, "f16"],
+ [Key.F17, "f17"],
+ [Key.F18, "f18"],
+ [Key.F19, "f19"],
+ [Key.F20, "f20"],
+ [Key.F21, "f21"],
+ [Key.F22, "f22"],
+ [Key.F23, "f23"],
+ [Key.F24, "f24"],
+
+ [Key.Num0, "0"],
+ [Key.Num1, "1"],
+ [Key.Num2, "2"],
+ [Key.Num3, "3"],
+ [Key.Num4, "4"],
+ [Key.Num5, "5"],
+ [Key.Num6, "6"],
+ [Key.Num7, "7"],
+ [Key.Num8, "8"],
+ [Key.Num9, "9"],
+ [Key.NumPad0, "numpad_0"],
+ [Key.NumPad1, "numpad_1"],
+ [Key.NumPad2, "numpad_2"],
+ [Key.NumPad3, "numpad_3"],
+ [Key.NumPad4, "numpad_4"],
+ [Key.NumPad5, "numpad_5"],
+ [Key.NumPad6, "numpad_6"],
+ [Key.NumPad7, "numpad_7"],
+ [Key.NumPad8, "numpad_8"],
+ [Key.NumPad9, "numpad_9"],
+
+ [Key.Space, "space"],
+ [Key.Escape, "escape"],
+ [Key.Tab, "tab"],
+ [Key.LeftAlt, "alt"],
+ [Key.LeftControl, "control"],
+ [Key.RightAlt, "alt"],
+ [Key.RightControl, "control"],
+
+ [Key.LeftShift, "shift"],
+ [Key.LeftSuper, "command"],
+ [Key.RightShift, "space"],
+ [Key.RightSuper, "command"],
+
+ [Key.Grave, "~"],
+ [Key.Minus, "-"],
+ [Key.Equal, "="],
+ [Key.Backspace, "backspace"],
+ [Key.LeftBracket, "["],
+ [Key.RightBracket, "]"],
+ [Key.Backslash, "\\"],
+ [Key.Semicolon, ";"],
+ [Key.Quote, "'"],
+ [Key.Return, "enter"],
+ [Key.Comma, ","],
+ [Key.Period, "."],
+ [Key.Slash, "/"],
+
+ [Key.Left, "left"],
+ [Key.Up, "up"],
+ [Key.Right, "right"],
+ [Key.Down, "down"],
+
+ [Key.Print, "printscreen"],
+ [Key.Pause, null],
+ [Key.Insert, "insert"],
+ [Key.Delete, "delete"],
+ [Key.Home, "home"],
+ [Key.End, "end"],
+ [Key.PageUp, "pageup"],
+ [Key.PageDown, "pagedown"],
+
+ [Key.Add, null],
+ [Key.Subtract, null],
+ [Key.Multiply, null],
+ [Key.Divide, null],
+ [Key.Decimal, null],
+ [Key.Enter, "enter"],
+
+ [Key.CapsLock, null],
+ [Key.ScrollLock, null],
+ [Key.NumLock, null],
+
+ [Key.AudioMute, "audio_mute"],
+ [Key.AudioVolDown, "audio_vol_down"],
+ [Key.AudioVolUp, "audio_vol_up"],
+ [Key.AudioPlay, "audio_play"],
+ [Key.AudioStop, "audio_stop"],
+ [Key.AudioPause, "audio_pause"],
+ [Key.AudioPrev, "audio_prev"],
+ [Key.AudioNext, "audio_next"],
+ [Key.AudioRewind, "audio_rewind"],
+ [Key.AudioForward, "audio_forward"],
+ [Key.AudioRepeat, "audio_repeat"],
+ [Key.AudioRandom, "audio_random"]
+ ]);
+
+ public static keyLookup(key: Key): any {
+ return this.KeyLookupMap.get(key);
+ }
+
+ private static mapModifierKeys(...keys: Key[]): string[] {
+ return keys
+ .map(modifier => KeyboardAction.keyLookup(modifier))
+ .filter(modifierKey => modifierKey != null && modifierKey.length > 1);
+ }
+
+ private static key(key: Key, event: "up" | "down", ...modifiers: Key[]): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const nativeKey = KeyboardAction.keyLookup(key);
+ const modifierKeys = this.mapModifierKeys(...modifiers);
+ if (nativeKey) {
+ libnut.keyToggle(nativeKey, event, modifierKeys);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ constructor() {
+ }
+
+ public type(input: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ libnut.typeString(input);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ public click(...keys: Key[]): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ keys.reverse();
+ const [key, ...modifiers] = keys;
+ const nativeKey = KeyboardAction.keyLookup(key);
+ const modifierKeys = KeyboardAction.mapModifierKeys(...modifiers);
+ if (nativeKey) {
+ libnut.keyTap(nativeKey, modifierKeys);
+ }
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ public pressKey(...keys: Key[]): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ keys.reverse();
+ const [key, ...modifiers] = keys;
+ await KeyboardAction.key(key, "down", ...modifiers);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ public releaseKey(...keys: Key[]): Promise {
+ return new Promise(async (resolve, reject) => {
+ try {
+ keys.reverse();
+ const [key, ...modifiers] = keys;
+ await KeyboardAction.key(key, "up", ...modifiers);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+
+ public setKeyboardDelay(delay: number): void {
+ libnut.setKeyboardDelay(delay);
+ }
+}
diff --git a/lib/provider/native/libnut-mouse-action.class.spec.ts b/lib/provider/native/libnut-mouse.class.spec.ts
similarity index 99%
rename from lib/provider/native/libnut-mouse-action.class.spec.ts
rename to lib/provider/native/libnut-mouse.class.spec.ts
index 4ceaa0e3..cd74c31e 100644
--- a/lib/provider/native/libnut-mouse-action.class.spec.ts
+++ b/lib/provider/native/libnut-mouse.class.spec.ts
@@ -1,7 +1,7 @@
import libnut = require("@nut-tree/libnut");
import { Button } from "../../button.enum";
import { Point } from "../../point.class";
-import MouseAction from "./libnut-mouse-action.class";
+import MouseAction from "./libnut-mouse.class";
jest.mock("@nut-tree/libnut");
diff --git a/lib/provider/native/libnut-mouse-action.class.ts b/lib/provider/native/libnut-mouse.class.ts
similarity index 95%
rename from lib/provider/native/libnut-mouse-action.class.ts
rename to lib/provider/native/libnut-mouse.class.ts
index 81d05cb9..c47bdbea 100644
--- a/lib/provider/native/libnut-mouse-action.class.ts
+++ b/lib/provider/native/libnut-mouse.class.ts
@@ -1,9 +1,9 @@
import libnut = require("@nut-tree/libnut");
import { Button } from "../../button.enum";
import { Point } from "../../point.class";
-import { MouseActionProvider } from "./mouse-action-provider.interface";
+import { MouseProviderInterface } from "../mouse-provider.interface";
-export default class MouseAction implements MouseActionProvider {
+export default class MouseAction implements MouseProviderInterface {
public static buttonLookup(btn: Button): any {
return this.ButtonLookupMap.get(btn);
}
diff --git a/lib/provider/native/libnut-screen-action.class.spec.ts b/lib/provider/native/libnut-screen.class.spec.ts
similarity index 98%
rename from lib/provider/native/libnut-screen-action.class.spec.ts
rename to lib/provider/native/libnut-screen.class.spec.ts
index a29df6a8..2e1aaa89 100644
--- a/lib/provider/native/libnut-screen-action.class.spec.ts
+++ b/lib/provider/native/libnut-screen.class.spec.ts
@@ -1,7 +1,8 @@
import libnut = require("@nut-tree/libnut");
import { Region } from "../../region.class";
-import ScreenAction from "./libnut-screen-action.class";
+import ScreenAction from "./libnut-screen.class";
+jest.mock("jimp", () => {});
jest.mock("@nut-tree/libnut");
beforeEach(() => {
diff --git a/lib/provider/native/libnut-screen-action.class.ts b/lib/provider/native/libnut-screen.class.ts
similarity index 87%
rename from lib/provider/native/libnut-screen-action.class.ts
rename to lib/provider/native/libnut-screen.class.ts
index a21b5359..1ab716a5 100644
--- a/lib/provider/native/libnut-screen-action.class.ts
+++ b/lib/provider/native/libnut-screen.class.ts
@@ -1,9 +1,10 @@
import libnut = require("@nut-tree/libnut");
-import { Image } from "../../image.class";
-import { Region } from "../../region.class";
-import { ScreenActionProvider } from "./screen-action-provider.interface";
+import {Image} from "../../image.class";
+import {Region} from "../../region.class";
+import {ScreenProviderInterface} from "../screen-provider.interface";
+import {ColorMode} from "../../colormode.enum";
-export default class ScreenAction implements ScreenActionProvider {
+export default class ScreenAction implements ScreenProviderInterface {
private static determinePixelDensity(
screen: Region,
@@ -33,6 +34,8 @@ export default class ScreenAction implements ScreenActionProvider {
screenShot.height,
screenShot.image,
4,
+ "grabScreenResult",
+ ColorMode.BGR,
pixelScaling,
),
);
@@ -58,6 +61,8 @@ export default class ScreenAction implements ScreenActionProvider {
screenShot.height,
screenShot.image,
4,
+ "grabScreenRegionResult",
+ ColorMode.BGR,
pixelScaling,
),
);
diff --git a/lib/provider/native/libnut-window-action.class.spec.ts b/lib/provider/native/libnut-window.class.spec.ts
similarity index 98%
rename from lib/provider/native/libnut-window-action.class.spec.ts
rename to lib/provider/native/libnut-window.class.spec.ts
index 93d4115b..a89a9deb 100644
--- a/lib/provider/native/libnut-window-action.class.spec.ts
+++ b/lib/provider/native/libnut-window.class.spec.ts
@@ -1,5 +1,5 @@
import libnut = require("@nut-tree/libnut");
-import WindowAction from "./libnut-window-action.class";
+import WindowAction from "./libnut-window.class";
import {Region} from "../../region.class";
jest.mock("@nut-tree/libnut");
diff --git a/lib/provider/native/libnut-window-action.class.ts b/lib/provider/native/libnut-window.class.ts
similarity index 88%
rename from lib/provider/native/libnut-window-action.class.ts
rename to lib/provider/native/libnut-window.class.ts
index 1352f682..40804364 100644
--- a/lib/provider/native/libnut-window-action.class.ts
+++ b/lib/provider/native/libnut-window.class.ts
@@ -1,8 +1,8 @@
import libnut = require("@nut-tree/libnut");
import { Region } from "../../region.class";
-import { WindowActionProvider } from "./window-action-provider.interface";
+import { WindowProviderInterface } from "../window-provider.interface";
-export default class WindowAction implements WindowActionProvider {
+export default class WindowAction implements WindowProviderInterface {
public getWindows(): Promise {
return new Promise((resolve, reject) => {
try {
diff --git a/lib/provider/opencv/__mocks__/alpha_channel.png b/lib/provider/opencv/__mocks__/alpha_channel.png
deleted file mode 100644
index 5214843c..00000000
Binary files a/lib/provider/opencv/__mocks__/alpha_channel.png and /dev/null differ
diff --git a/lib/provider/opencv/__mocks__/coverage.png b/lib/provider/opencv/__mocks__/coverage.png
deleted file mode 100644
index e23cd669..00000000
Binary files a/lib/provider/opencv/__mocks__/coverage.png and /dev/null differ
diff --git a/lib/provider/opencv/__mocks__/downloads.png b/lib/provider/opencv/__mocks__/downloads.png
deleted file mode 100644
index d8542250..00000000
Binary files a/lib/provider/opencv/__mocks__/downloads.png and /dev/null differ
diff --git a/lib/provider/opencv/__mocks__/fat-needle.png b/lib/provider/opencv/__mocks__/fat-needle.png
deleted file mode 100644
index 87533253..00000000
Binary files a/lib/provider/opencv/__mocks__/fat-needle.png and /dev/null differ
diff --git a/lib/provider/opencv/__mocks__/mouse.png b/lib/provider/opencv/__mocks__/mouse.png
deleted file mode 100644
index b3468b75..00000000
Binary files a/lib/provider/opencv/__mocks__/mouse.png and /dev/null differ
diff --git a/lib/provider/opencv/__mocks__/needle.png b/lib/provider/opencv/__mocks__/needle.png
deleted file mode 100644
index a9b012f0..00000000
Binary files a/lib/provider/opencv/__mocks__/needle.png and /dev/null differ
diff --git a/lib/provider/opencv/bound-value.function.spec.ts b/lib/provider/opencv/bound-value.function.spec.ts
deleted file mode 100644
index fc5f9323..00000000
--- a/lib/provider/opencv/bound-value.function.spec.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { lowerBound, upperBound } from "./bound-value.function";
-
-describe("lowerBound function", () => {
- it.each([
- [5, 10, 1, 1],
- [5, 5, 10, 10],
- [5, 1, 10, 5],
- [0, 0, 0, 0]
- ])("Input: %f, Boundary: %f, minValue: %f, Expected: %f",
- (input: number, boundary: number, minValue: number, expected: number) => {
- // WHEN
- const result = lowerBound(input, boundary, minValue);
-
- // THEN
- expect(result).toBe(expected);
- });
-});
-
-describe("upperBound function", () => {
- it.each([
- [5, 10, 1, 5],
- [5, 5, 10, 10],
- [5, 1, 10, 10],
- [5, 5, 5, 5]
- ])("Input: %f, Boundary: %f, maxValue: %f, Expected: %f",
- (input: number, boundary: number, maxValue: number, expected: number) => {
- // WHEN
- const result = upperBound(input, boundary, maxValue);
-
- // THEN
- expect(result).toBe(expected);
- });
-});
diff --git a/lib/provider/opencv/bound-value.function.ts b/lib/provider/opencv/bound-value.function.ts
deleted file mode 100644
index e6e6b308..00000000
--- a/lib/provider/opencv/bound-value.function.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function lowerBound(value: number, boundary: number, minValue: number): number {
- return (value <= boundary) ? minValue : value;
-}
-
-export function upperBound(value: number, boundary: number, maxValue: number): number {
- return (value >= boundary) ? maxValue : value;
-}
diff --git a/lib/provider/opencv/data-sink.interface.ts b/lib/provider/opencv/data-sink.interface.ts
deleted file mode 100644
index 79f15b1c..00000000
--- a/lib/provider/opencv/data-sink.interface.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * A DataSink should provide methods to store data
- *
- * @interface DataSink
- */
-export interface DataSink {
- /**
- * store will write data to disk
- * @param data Data to write
- * @param path Absolute output file path
- */
- store(data: any, path: string): Promise;
-}
diff --git a/lib/provider/opencv/data-source.interface.ts b/lib/provider/opencv/data-source.interface.ts
deleted file mode 100644
index f59d06b7..00000000
--- a/lib/provider/opencv/data-source.interface.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * A DataSource should provide methods to load data
- *
- * @interface DataSource
- */
-export interface DataSource {
- /**
- * load will load image data from disk
- * @param path Absolute path to output file
- */
- load(path: string): Promise;
-}
diff --git a/lib/provider/opencv/determine-searchregion.function.spec.ts b/lib/provider/opencv/determine-searchregion.function.spec.ts
deleted file mode 100644
index 9885aec8..00000000
--- a/lib/provider/opencv/determine-searchregion.function.spec.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { mockPartial } from "sneer";
-import { Image } from "../../image.class";
-import { MatchRequest } from "../../match-request.class";
-import { Region } from "../../region.class";
-import { determineScaledSearchRegion } from "./determine-searchregion.function";
-
-describe("determineSearchRegion", () => {
- it("should return a search region adopted to pixel density", () => {
- // GIVEN
- const imageMock = mockPartial({
- pixelDensity: {
- scaleX: 1.5,
- scaleY: 2.0
- }
- });
- const needlePath = "/path/to/needle";
- const inputSearchRegion = new Region(0, 0, 100, 100);
- const expectedSearchRegion = new Region(0, 0, 150, 200);
-
- const matchRequest = new MatchRequest(
- imageMock,
- needlePath,
- inputSearchRegion,
- 0.99
- );
-
- // WHEN
- const result = determineScaledSearchRegion(matchRequest);
-
- // THEN
- expect(result).toEqual(expectedSearchRegion);
- });
-
- it.each([[0, 1], [1, 0]])("should not adjust searchregion for factor 0: scaleX: %i scaleY: %i",
- (scaleX: number, scaleY: number) => {
- // GIVEN
- const imageMock = mockPartial({
- pixelDensity: {
- scaleX,
- scaleY
- }
- });
- const needlePath = "/path/to/needle";
- const inputSearchRegion = new Region(0, 0, 100, 100);
- const expectedSearchRegion = new Region(0, 0, 100, 100);
-
- const matchRequest = new MatchRequest(
- imageMock,
- needlePath,
- inputSearchRegion,
- 0.99
- );
-
- // WHEN
- const result = determineScaledSearchRegion(matchRequest);
-
- // THEN
- expect(result).toEqual(expectedSearchRegion);
- });
-});
diff --git a/lib/provider/opencv/determine-searchregion.function.ts b/lib/provider/opencv/determine-searchregion.function.ts
deleted file mode 100644
index 84226b1d..00000000
--- a/lib/provider/opencv/determine-searchregion.function.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { MatchRequest } from "../../match-request.class";
-import { Region } from "../../region.class";
-
-export function determineScaledSearchRegion(matchRequest: MatchRequest): Region {
- const searchRegion = matchRequest.searchRegion;
- const scaleX = matchRequest.haystack.pixelDensity.scaleX || 1.0;
- const scaleY = matchRequest.haystack.pixelDensity.scaleY || 1.0;
- searchRegion.width *= scaleX;
- searchRegion.height *= scaleY;
- return searchRegion;
-}
diff --git a/lib/provider/opencv/finder.interface.ts b/lib/provider/opencv/finder.interface.ts
deleted file mode 100644
index 83da0d00..00000000
--- a/lib/provider/opencv/finder.interface.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { MatchRequest } from "../../match-request.class";
-import { MatchResult } from "../../match-result.class";
-
-/**
- * A Finder should provide an abstraction layer to perform
- * image processing and matching via a 3rd part library
- *
- * @interface FinderInterface
- */
-export interface FinderInterface {
- /**
- * findMatch should provide an abstraction to search for an image needle
- * in another image haystack
- *
- * @param {MatchRequest} matchRequest A matchrequest containing needed matching data
- * @returns {Promise} A matchresult containing the match probability and location
- * @memberof FinderInterface
- */
- findMatch(matchRequest: MatchRequest): Promise;
-
- /**
- * findMatches should provide an abstraction to search for an image needle
- * in another image haystack
- *
- * @param {MatchRequest} matchRequest A matchrequest containing needed matching data
- * @returns {Promise} A list of matchresults containing the match probability and location
- * @memberof FinderInterface
- */
- findMatches(matchRequest: MatchRequest): Promise;
-}
diff --git a/lib/provider/opencv/image-processor.class.spec.ts b/lib/provider/opencv/image-processor.class.spec.ts
deleted file mode 100644
index 19651f7d..00000000
--- a/lib/provider/opencv/image-processor.class.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {resolve} from "path";
-import {Region} from "../../region.class";
-import {ImageReader} from "./image-reader.class";
-import {fromImageWithAlphaChannel, fromImageWithoutAlphaChannel} from "./image-processor.class";
-
-describe("ImageProcessor", () => {
- it("should allow to create a cv.Mat from an Image with alpha channel, alpha channel is dropped", async () => {
- // GIVEN
- const imageReader = new ImageReader();
- const imagePath = resolve(__dirname, "./__mocks__/alpha_channel.png");
- const image = await imageReader.load(imagePath);
-
- // WHEN
- const mat = await fromImageWithAlphaChannel(image);
-
- // THEN
- expect(image.hasAlphaChannel).toBeTruthy();
- expect(mat.channels).toEqual(3);
- expect(mat.rows).toEqual(image.height);
- expect(mat.cols).toEqual(image.width);
- expect(mat.empty).toBeFalsy();
- });
-
- it("should allow to create a cv.Mat from an Image without alpha channel", async () => {
- // GIVEN
- const imageReader = new ImageReader();
- const imagePath = resolve(__dirname, "./__mocks__/mouse.png");
- const image = await imageReader.load(imagePath);
-
- // WHEN
- const mat = await fromImageWithoutAlphaChannel(image);
-
- // THEN
- expect(image.hasAlphaChannel).toBeFalsy();
- expect(mat.channels).toEqual(3);
- expect(mat.rows).toEqual(image.height);
- expect(mat.cols).toEqual(image.width);
- expect(mat.empty).toBeFalsy();
- });
-});
-
-describe("ImageProcessor with ROI", () => {
- it("negative left or top values are updated to 0", async () => {
- // GIVEN
- const imageReader = new ImageReader();
- const imagePath = resolve(__dirname, "./__mocks__/mouse.png");
- const image = await imageReader.load(imagePath);
-
- // WHEN
- const mat = await fromImageWithoutAlphaChannel(
- image,
- new Region(-100, -100, 10, 10)
- );
-
- // THEN
- expect(image.hasAlphaChannel).toBeFalsy();
- expect(mat.channels).toEqual(3);
- expect(mat.rows).toEqual(10);
- expect(mat.cols).toEqual(10);
- expect(mat.empty).toBeFalsy();
- });
-
- it("values bigger than the input are updated to width and height", async () => {
- // GIVEN
- const imageReader = new ImageReader();
- const imagePath = resolve(__dirname, "./__mocks__/mouse.png");
- const image = await imageReader.load(imagePath);
-
- // WHEN
- const mat = await fromImageWithoutAlphaChannel(
- image,
- new Region(10, 10, image.width * 2, image.height * 2)
- );
-
- // THEN
- expect(image.hasAlphaChannel).toBeFalsy();
- expect(mat.channels).toEqual(3);
- expect(mat.rows).toEqual(image.height - 10);
- expect(mat.cols).toEqual(image.width - 10);
- expect(mat.empty).toBeFalsy();
- });
-});
diff --git a/lib/provider/opencv/image-processor.class.ts b/lib/provider/opencv/image-processor.class.ts
deleted file mode 100644
index 7b0a3ab6..00000000
--- a/lib/provider/opencv/image-processor.class.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import {Image} from "../../image.class";
-import {Region} from "../../region.class";
-
-function determineROI(img: Image, roi: Region): cv.Rect {
- return new cv.Rect(
- Math.min(Math.max(roi.left, 0), img.width),
- Math.min(Math.max(roi.top, 0), img.height),
- Math.min(roi.width, img.width - roi.left),
- Math.min(roi.height, img.height - roi.top));
-}
-
-/**
- * fromImageWithAlphaChannel should provide a way to create a library specific
- * image with alpha channel from an abstract Image object holding raw data and image dimension
- *
- * @param {Image} img The input Image
- * @param {Region} [roi] An optional Region to specify a ROI
- * @returns {Promise} An image
- * @memberof VisionProviderInterface
- */
-export const fromImageWithAlphaChannel = async (
- img: Image,
- roi?: Region,
-): Promise => {
- const mat = await new cv.Mat(img.data, img.height, img.width, cv.CV_8UC4).cvtColorAsync(cv.COLOR_BGRA2BGR);
- if (roi) {
- return mat.getRegion(determineROI(img, roi));
- } else {
- return mat;
- }
-};
-
-/**
- * fromImageWithoutAlphaChannel should provide a way to create a library specific
- * image without alpha channel from an abstract Image object holding raw data and image dimension
- *
- * @param {Image} img The input Image
- * @param {Region} [roi] An optional Region to specify a ROI
- * @returns {Promise} An image
- * @memberof VisionProviderInterface
- */
-export const fromImageWithoutAlphaChannel = async (
- img: Image,
- roi?: Region,
-): Promise => {
- const mat = new cv.Mat(img.data, img.height, img.width, cv.CV_8UC3);
- if (roi) {
- return mat.getRegion(determineROI(img, roi));
- } else {
- return mat;
- }
-};
diff --git a/lib/provider/opencv/image-reader.class.spec.ts b/lib/provider/opencv/image-reader.class.spec.ts
deleted file mode 100644
index f51d93bb..00000000
--- a/lib/provider/opencv/image-reader.class.spec.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as path from "path";
-import { ImageReader } from "./image-reader.class";
-
-describe("Image loader", () => {
- it("should resolve to a non-empty Mat on successful load", async () => {
- // GIVEN
- const SUT = new ImageReader();
- const imagePath = path.resolve(__dirname, "./__mocks__/mouse.png");
-
- // WHEN
- const result = await SUT.load(imagePath);
-
- // THEN
- expect(result.height).toBeGreaterThan(0);
- expect(result.width).toBeGreaterThan(0);
- });
-
- it("loadImage should reject on unsuccessful load", async () => {
- // GIVEN
- const SUT = new ImageReader();
- const imagePath = "./__mocks__/foo.png";
-
- // WHEN
- const call = SUT.load;
-
- // THEN
- await expect(call(imagePath)).rejects.toEqual(`Failed to load image from '${imagePath}'`);
- });
-});
diff --git a/lib/provider/opencv/image-reader.class.ts b/lib/provider/opencv/image-reader.class.ts
deleted file mode 100644
index 73d90c34..00000000
--- a/lib/provider/opencv/image-reader.class.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import { Image } from "../../image.class";
-import { DataSource } from "./data-source.interface";
-
-export class ImageReader implements DataSource {
- public async load(path: string): Promise {
- return new Promise(async (resolve, reject) => {
- try {
- const image = await cv.imreadAsync(path, cv.IMREAD_UNCHANGED);
- resolve(new Image(image.cols, image.rows, image.getData(), image.channels));
- } catch (e) {
- reject(`Failed to load image from '${path}'`);
- }
- });
- }
-}
diff --git a/lib/provider/opencv/image-writer.class.spec.ts b/lib/provider/opencv/image-writer.class.spec.ts
deleted file mode 100644
index d672ed9a..00000000
--- a/lib/provider/opencv/image-writer.class.spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { existsSync, unlinkSync } from "fs";
-import { resolve } from "path";
-import { ImageReader } from "./image-reader.class";
-import { ImageWriter } from "./image-writer.class";
-
-const INPUT_PATH = resolve(__dirname, "./__mocks__/mouse.png");
-const OUTPUT_PATH_PNG = resolve(__dirname, "./__mocks__/output.png");
-const OUTPUT_PATH_JPG = resolve(__dirname, "./__mocks__/output.jpg");
-
-beforeEach(() => {
- for (const file of [OUTPUT_PATH_JPG, OUTPUT_PATH_PNG]) {
- if (existsSync(file)) {
- unlinkSync(file);
- }
- }
-});
-
-describe.each([[OUTPUT_PATH_PNG], [OUTPUT_PATH_JPG]])(
- "Image writer", (outputPath) => {
- test("should allow to store image data to disk", async () => {
- // GIVEN
- const imageReader = new ImageReader();
- const image = await imageReader.load(INPUT_PATH);
- const imageWriter = new ImageWriter();
-
- // WHEN
- await imageWriter.store(image, outputPath);
-
- // THEN
- expect(existsSync(outputPath)).toBeTruthy();
- });
- });
diff --git a/lib/provider/opencv/image-writer.class.ts b/lib/provider/opencv/image-writer.class.ts
deleted file mode 100644
index 6ae6b90c..00000000
--- a/lib/provider/opencv/image-writer.class.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import {Image} from "../../image.class";
-import {DataSink} from "./data-sink.interface";
-import {fromImageWithAlphaChannel, fromImageWithoutAlphaChannel} from "./image-processor.class";
-
-export class ImageWriter implements DataSink {
- public async store(data: Image, path: string): Promise {
- let outputMat: cv.Mat;
- if (data.hasAlphaChannel) {
- outputMat = await fromImageWithAlphaChannel(data);
- } else {
- outputMat = await fromImageWithoutAlphaChannel(data);
- }
- return cv.imwriteAsync(path, outputMat);
- }
-}
diff --git a/lib/provider/opencv/match-image.function.spec.ts b/lib/provider/opencv/match-image.function.spec.ts
deleted file mode 100644
index 41baa7e1..00000000
--- a/lib/provider/opencv/match-image.function.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import { mockPartial } from "sneer";
-import { matchImages } from "./match-image.function";
-
-describe("matchImages", () => {
- it("should return minLoc position and needle size", async () => {
- // GIVEN
- const minLocX = 100;
- const minLocY = 1000;
- const matchMock = mockPartial({
- minMaxLocAsync: jest.fn(() => Promise.resolve({
- maxLoc: new cv.Point2(
- 200,
- 2000
- ),
- maxVal: 100,
- minLoc: new cv.Point2(
- minLocX,
- minLocY
- ),
- minVal: 0,
- }))
- });
- const haystackMock = mockPartial({
- matchTemplateAsync: jest.fn(() => Promise.resolve(matchMock))
- });
- const needleMock = mockPartial({
- cols: 123,
- rows: 456
- });
-
- // WHEN
- const result = await matchImages(haystackMock, needleMock);
-
- // THEN
- expect(result.location.left).toEqual(minLocX);
- expect(result.location.top).toEqual(minLocY);
- expect(result.location.width).toEqual(needleMock.cols);
- expect(result.location.height).toEqual(needleMock.rows);
- });
-});
diff --git a/lib/provider/opencv/match-image.function.ts b/lib/provider/opencv/match-image.function.ts
deleted file mode 100644
index c49e422a..00000000
--- a/lib/provider/opencv/match-image.function.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import { MatchResult } from "../../match-result.class";
-import { Region } from "../../region.class";
-
-export async function matchImages(haystack: cv.Mat, needle: cv.Mat): Promise {
- const match = await haystack.matchTemplateAsync(
- needle,
- cv.TM_SQDIFF_NORMED,
- );
- const minMax = await match.minMaxLocAsync();
- return new MatchResult(
- 1.0 - minMax.minVal,
- new Region(
- minMax.minLoc.x,
- minMax.minLoc.y,
- needle.cols,
- needle.rows,
- ),
- );
-}
diff --git a/lib/provider/opencv/scale-image.function.spec.ts b/lib/provider/opencv/scale-image.function.spec.ts
deleted file mode 100644
index c25f4dee..00000000
--- a/lib/provider/opencv/scale-image.function.spec.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as path from "path";
-import {ImageReader} from "./image-reader.class";
-import {scaleImage} from "./scale-image.function";
-import {fromImageWithoutAlphaChannel} from "./image-processor.class";
-
-describe("scaleImage", () => {
- it.each([[0.5], [1.5]])("should scale an image correctly by factor %f", async (scaleFactor) => {
- // GIVEN
- const imageLoader = new ImageReader();
- const pathToinput = path.resolve(__dirname, "./__mocks__/mouse.png");
- const inputImage = await imageLoader.load(pathToinput);
- const inputMat = await fromImageWithoutAlphaChannel(inputImage);
- const expectedWidth = Math.floor(inputMat.cols * scaleFactor);
- const expectedHeight = Math.floor(inputMat.rows * scaleFactor);
-
- // WHEN
- const result = await scaleImage(inputMat, scaleFactor);
-
- // THEN
- expect(result.rows).toBe(expectedHeight);
- expect(result.cols).toBe(expectedWidth);
- });
-
- it.each([[0], [-0.25]])("should keep scale if factor <= 0: Scale %f", async (scaleFactor) => {
- // GIVEN
- const imageLoader = new ImageReader();
- const pathToinput = path.resolve(__dirname, "./__mocks__/mouse.png");
- const inputImage = await imageLoader.load(pathToinput);
- const inputMat = await fromImageWithoutAlphaChannel(inputImage);
- const expectedWidth = inputMat.cols;
- const expectedHeight = inputMat.rows;
-
- // WHEN
- const result = await scaleImage(inputMat, scaleFactor);
-
- // THEN
- expect(result.rows).toBe(expectedHeight);
- expect(result.cols).toBe(expectedWidth);
- });
-});
diff --git a/lib/provider/opencv/scale-image.function.ts b/lib/provider/opencv/scale-image.function.ts
deleted file mode 100644
index b5fbd21d..00000000
--- a/lib/provider/opencv/scale-image.function.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import { lowerBound } from "./bound-value.function";
-
-export async function scaleImage(image: cv.Mat, scaleFactor: number): Promise {
- const boundScaleFactor = lowerBound(scaleFactor, 0.0, 1.0);
- const scaledRows = Math.floor(image.rows * boundScaleFactor);
- const scaledCols = Math.floor(image.cols * boundScaleFactor);
- return image.resizeAsync(scaledRows, scaledCols, 0, 0, cv.INTER_AREA);
-}
diff --git a/lib/provider/opencv/scale-location.function.spec.ts b/lib/provider/opencv/scale-location.function.spec.ts
deleted file mode 100644
index dfdf3e8a..00000000
--- a/lib/provider/opencv/scale-location.function.spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Region } from "../../region.class";
-import { scaleLocation } from "./scale-location.function";
-
-describe("scaleLocation", () => {
- it("should scale location of a Region for valid scale factors", () => {
- // GIVEN
- const scaleFactor = 0.5;
- const inputRegion = new Region(100, 100, 10, 10);
- const expectedRegion = new Region(200, 200, 10, 10);
-
- // WHEN
- const result = scaleLocation(inputRegion, scaleFactor);
-
- // THEN
- expect(result).toEqual(expectedRegion);
- });
-
- it("should not scale location of a Region for invalid scale factors", () => {
- // GIVEN
- const scaleFactor = 0.0;
- const inputRegion = new Region(100, 100, 10, 10);
- const expectedRegion = new Region(100, 100, 10, 10);
-
- // WHEN
- const result = scaleLocation(inputRegion, scaleFactor);
-
- // THEN
- expect(result).toEqual(expectedRegion);
- });
-});
diff --git a/lib/provider/opencv/scale-location.function.ts b/lib/provider/opencv/scale-location.function.ts
deleted file mode 100644
index 814160c0..00000000
--- a/lib/provider/opencv/scale-location.function.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Region } from "../../region.class";
-import { lowerBound } from "./bound-value.function";
-
-export function scaleLocation(
- result: Region,
- scaleFactor: number,
-): Region {
- const boundScaleFactor = lowerBound(scaleFactor, 0.0, 1.0);
- return new Region(
- result.left / boundScaleFactor,
- result.top / boundScaleFactor,
- result.width,
- result.height,
- );
-}
diff --git a/lib/provider/opencv/template-matching-finder.class.spec.ts b/lib/provider/opencv/template-matching-finder.class.spec.ts
deleted file mode 100644
index 974dc897..00000000
--- a/lib/provider/opencv/template-matching-finder.class.spec.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import * as path from "path";
-import {Image} from "../../image.class";
-import {MatchRequest} from "../../match-request.class";
-import {Region} from "../../region.class";
-import {ImageReader} from "./image-reader.class";
-import TemplateMatchingFinder from "./template-matching-finder.class";
-
-describe("Template-matching finder", () => {
- it("findMatch should return a match when present in image", async () => {
- // GIVEN
- const imageLoader = new ImageReader();
- const SUT = new TemplateMatchingFinder();
- const haystackPath = path.resolve(__dirname, "./__mocks__/mouse.png");
- const needlePath = path.resolve(__dirname, "./__mocks__/needle.png");
- const haystack = await imageLoader.load(haystackPath);
- const needle = await imageLoader.load(needlePath);
- const minConfidence = 0.99;
- const searchRegion = new Region(0, 0, haystack.width, haystack.height);
- const matchRequest = new MatchRequest(haystack, needlePath, searchRegion, minConfidence);
- const expectedResult = new Region(16, 31, needle.width, needle.height);
-
- // WHEN
- const result = await SUT.findMatch(matchRequest);
-
- // THEN
- expect(result.confidence).toBeGreaterThanOrEqual(minConfidence);
- expect(result.location).toEqual(expectedResult);
- });
-
- it("findMatch should return a match within a search region when present in image", async () => {
- // GIVEN
- const imageLoader = new ImageReader();
- const SUT = new TemplateMatchingFinder();
- const haystackPath = path.resolve(__dirname, "./__mocks__/mouse.png");
- const needlePath = path.resolve(__dirname, "./__mocks__/needle.png");
- const haystack = await imageLoader.load(haystackPath);
- const needle = await imageLoader.load(needlePath);
- const minConfidence = 0.99;
- const searchRegion = new Region(10, 20, 140, 100);
- const matchRequest = new MatchRequest(haystack, needlePath, searchRegion, minConfidence);
- const expectedResult = new Region(6, 11, needle.width, needle.height);
-
- // WHEN
- const result = await SUT.findMatch(matchRequest);
-
- // THEN
- expect(result.confidence).toBeGreaterThanOrEqual(minConfidence);
- expect(result.location).toEqual(expectedResult);
- });
-
- it("findMatch should return confidence and location of best match if no match with sufficient confidence is found", async () => {
- // GIVEN
- const imageLoader = new ImageReader();
- const SUT = new TemplateMatchingFinder();
- const haystackPath = path.resolve(__dirname, "./__mocks__/downloads.png");
- const needlePath = path.resolve(__dirname, "./__mocks__/coverage.png");
- const haystack = await imageLoader.load(haystackPath);
- const minConfidence = 0.99;
- const searchRegion = new Region(0, 0, 320, 72);
- const matchRequest = new MatchRequest(haystack, needlePath, searchRegion, minConfidence);
- const expectedRejection = new RegExp(`^No match with required confidence ${minConfidence}. Best match: \\d.\\d* at \\(\\d*, \\d*, \\d*, \\d*\\)$`)
-
- // WHEN
-
- // THEN
- await expect(SUT.findMatch(matchRequest))
- .rejects
- .toThrowError(expectedRejection);
- });
-
- it("findMatch should throw on invalid image paths", async () => {
- // GIVEN
- const imageLoader = new ImageReader();
- const SUT = new TemplateMatchingFinder();
- const pathToNeedle = path.resolve(__dirname, "./__mocks__/mouse.png");
- const pathToHaystack = "./__mocks__/foo.png";
- const needle = await imageLoader.load(pathToNeedle);
- const minConfidence = 0.99;
- const searchRegion = new Region(0, 0, 100, 100);
- const haystack = new Image(needle.width, needle.height, needle.data, 3);
- const matchRequest = new MatchRequest(haystack, pathToHaystack, searchRegion, minConfidence);
-
- // WHEN
- const result = SUT.findMatch(matchRequest);
-
- // THEN
- await expect(result)
- .rejects
- .toThrowError(`Failed to load ${pathToHaystack}. Reason: 'Failed to load image from '${pathToHaystack}''.`);
- });
-
- it("findMatch should reject, if needle was way lager than the haystack", async () => {
- // GIVEN
- const imageLoader = new ImageReader();
- const SUT = new TemplateMatchingFinder();
- const haystackPath = path.resolve(__dirname, "./__mocks__/mouse.png");
- const needlePath = path.resolve(__dirname, "./__mocks__/fat-needle.png");
- const haystack = await imageLoader.load(haystackPath);
- const minConfidence = 0.99;
- const searchRegion = new Region(0, 0, haystack.width, haystack.height);
- const matchRequest = new MatchRequest(haystack, needlePath, searchRegion, minConfidence);
- const expectedRejection = new Error("The provided image sample is larger than the provided search region")
-
- // WHEN
- const findMatchPromise = SUT.findMatch(matchRequest);
-
- // THEN
- await expect(findMatchPromise).rejects.toEqual(expectedRejection)
- });
-});
diff --git a/lib/provider/opencv/template-matching-finder.class.ts b/lib/provider/opencv/template-matching-finder.class.ts
deleted file mode 100755
index 4ea34976..00000000
--- a/lib/provider/opencv/template-matching-finder.class.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import * as cv from "opencv4nodejs-prebuilt";
-import * as path from "path";
-import {Image} from "../../image.class";
-import {MatchRequest} from "../../match-request.class";
-import {MatchResult} from "../../match-result.class";
-import {Region} from "../../region.class";
-import {ScaledMatchResult} from "../../scaled-match-result.class";
-import {DataSource} from "./data-source.interface";
-import {determineScaledSearchRegion} from "./determine-searchregion.function";
-import {FinderInterface} from "./finder.interface";
-import {ImageReader} from "./image-reader.class";
-import {matchImages} from "./match-image.function";
-import {scaleImage} from "./scale-image.function";
-import {scaleLocation} from "./scale-location.function";
-import {fromImageWithAlphaChannel, fromImageWithoutAlphaChannel} from "./image-processor.class";
-
-async function loadNeedle(image: Image): Promise {
- if (image.hasAlphaChannel) {
- return fromImageWithAlphaChannel(image);
- }
- return fromImageWithoutAlphaChannel(image);
-}
-
-async function loadHaystack(matchRequest: MatchRequest): Promise {
- const searchRegion = determineScaledSearchRegion(matchRequest);
- if (matchRequest.haystack.hasAlphaChannel) {
- return fromImageWithAlphaChannel(
- matchRequest.haystack,
- searchRegion,
- );
- } else {
- return fromImageWithoutAlphaChannel(
- matchRequest.haystack,
- searchRegion,
- );
- }
-}
-
-function debugImage(image: cv.Mat, filename: string, suffix?: string) {
- const parsedPath = path.parse(filename);
- let fullFilename = parsedPath.name;
- if (suffix) {
- fullFilename = fullFilename + "_" + suffix;
- }
- fullFilename += parsedPath.ext;
- const fullPath = path.join(parsedPath.dir, fullFilename);
- cv.imwriteAsync(fullPath, image);
-}
-
-// function debugResult(image: cv.Mat, result: MatchResult, filename: string, suffix?: string) {
-// const roiRect = new cv.Rect(
-// Math.min(Math.max(result.location.left, 0), image.cols),
-// Math.min(Math.max(result.location.top, 0), image.rows),
-// Math.min(result.location.width, image.cols - result.location.left),
-// Math.min(result.location.height, image.rows - result.location.top));
-// debugImage(image.getRegion(roiRect), filename, suffix);
-// }
-
-function isValidSearch(needle: cv.Mat, haystack: cv.Mat): boolean {
- return (needle.cols <= haystack.cols) && (needle.rows <= haystack.rows);
-}
-
-function createResultForInvalidSearch (currentScale: number) {
- return new ScaledMatchResult(0,
- currentScale,
- new Region(
- 0,
- 0,
- 0,
- 0
- ),
- new Error("The provided image sample is larger than the provided search region")
- )
-}
-
-export default class TemplateMatchingFinder implements FinderInterface {
- private initialScale = [1.0];
- private scaleSteps = [0.9, 0.8, 0.7, 0.6, 0.5];
-
- constructor(
- private source: DataSource = new ImageReader(),
- ) {
- }
-
- public async findMatches(matchRequest: MatchRequest, debug: boolean = false): Promise {
- let needle: cv.Mat;
- try {
- const needleInput = await this.source.load(matchRequest.pathToNeedle);
- needle = await loadNeedle(needleInput);
- } catch (e) {
- throw new Error(
- `Failed to load ${matchRequest.pathToNeedle}. Reason: '${e}'.`,
- );
- }
- if (!needle || needle.empty) {
- throw new Error(
- `Failed to load ${matchRequest.pathToNeedle}, got empty image.`,
- );
- }
- const haystack = await loadHaystack(matchRequest);
-
- if (debug) {
- debugImage(needle, "input_needle.png");
- debugImage(haystack, "input_haystack.png");
- }
-
- const matchResults = this.initialScale.map(
- async (currentScale) => {
- if (!isValidSearch(needle, haystack)) {
- return createResultForInvalidSearch(currentScale);
- }
- const matchResult = await matchImages(haystack, needle);
- return new ScaledMatchResult(matchResult.confidence, currentScale, matchResult.location);
- }
- );
-
- if (matchRequest.searchMultipleScales) {
- matchResults.push(...this.searchMultipleScales(needle, haystack))
- }
-
- return Promise.all(matchResults).then(results => {
- results.forEach(matchResult => {
- matchResult.location.left /= matchRequest.haystack.pixelDensity.scaleX;
- matchResult.location.width /= matchRequest.haystack.pixelDensity.scaleX;
- matchResult.location.top /= matchRequest.haystack.pixelDensity.scaleY;
- matchResult.location.height /= matchRequest.haystack.pixelDensity.scaleY;
- });
- return results.sort(
- (first, second) => second.confidence - first.confidence,
- );
- });
- }
-
- public async findMatch(matchRequest: MatchRequest, debug: boolean = false): Promise {
-
- const matches = await this.findMatches(matchRequest, debug);
- const potentialMatches = matches
- .filter(match => match.confidence >= matchRequest.confidence);
- if (potentialMatches.length === 0) {
- matches.sort((a, b) => a.confidence - b.confidence);
- const bestMatch = matches.pop();
- if (bestMatch) {
- if(bestMatch.error) {
- throw bestMatch.error
- }else {
- throw new Error(`No match with required confidence ${matchRequest.confidence}. Best match: ${bestMatch.confidence} at ${bestMatch.location}`)
- }
- } else {
- throw new Error(`Unable to locate ${matchRequest.pathToNeedle}, no match!`);
- }
- }
- return potentialMatches[0];
- }
-
- private searchMultipleScales(needle: cv.Mat, haystack: cv.Mat) {
- const scaledNeedleResult = this.scaleSteps.map(
- async (currentScale) => {
- const scaledNeedle = await scaleImage(needle, currentScale);
- if (!isValidSearch(scaledNeedle, haystack)) {
- return createResultForInvalidSearch(currentScale);
- }
- const matchResult = await matchImages(haystack, scaledNeedle);
- return new ScaledMatchResult(
- matchResult.confidence,
- currentScale,
- matchResult.location,
- );
- }
- );
- const scaledHaystackResult = this.scaleSteps.map(
- async (currentScale) => {
- const scaledHaystack = await scaleImage(haystack, currentScale);
- if (!isValidSearch(needle, scaledHaystack)) {
- return createResultForInvalidSearch(currentScale);
- }
- const matchResult = await matchImages(scaledHaystack, needle);
- return new ScaledMatchResult(
- matchResult.confidence,
- currentScale,
- scaleLocation(
- matchResult.location,
- currentScale
- )
- );
- }
- );
- return [...scaledHaystackResult, ...scaledNeedleResult];
- }
-}
diff --git a/lib/provider/provider-registry.class.ts b/lib/provider/provider-registry.class.ts
new file mode 100644
index 00000000..17aa1c1f
--- /dev/null
+++ b/lib/provider/provider-registry.class.ts
@@ -0,0 +1,181 @@
+import {ClipboardProviderInterface} from "./clipboard-provider.interface";
+import {ImageFinderInterface} from "./image-finder.interface";
+import {KeyboardProviderInterface} from "./keyboard-provider.interface";
+import {MouseProviderInterface} from "./mouse-provider.interface";
+import {ScreenProviderInterface} from "./screen-provider.interface";
+import {WindowProviderInterface} from "./window-provider.interface";
+
+import Clipboard from "./native/clipboardy-clipboard.class";
+import Mouse from "./native/libnut-mouse.class";
+import Keyboard from "./native/libnut-keyboard.class";
+import Screen from "./native/libnut-screen.class";
+import Window from "./native/libnut-window.class";
+import {ImageReader} from "./image-reader.type";
+import {ImageWriter} from "./image-writer.type";
+import {ImageProcessor} from "./image-processor.interface";
+
+import ImageReaderImpl from "./io/jimp-image-reader.class";
+import ImageWriterImpl from "./io/jimp-image-writer.class";
+import ImageProcessorImpl from "./image/jimp-image-processor.class";
+
+export interface ProviderRegistry {
+ getClipboard(): ClipboardProviderInterface;
+
+ registerClipboardProvider(value: ClipboardProviderInterface): void;
+
+ getKeyboard(): KeyboardProviderInterface;
+
+ registerKeyboardProvider(value: KeyboardProviderInterface): void;
+
+ getMouse(): MouseProviderInterface;
+
+ registerMouseProvider(value: MouseProviderInterface): void;
+
+ getScreen(): ScreenProviderInterface;
+
+ registerScreenProvider(value: ScreenProviderInterface): void;
+
+ getWindow(): WindowProviderInterface;
+
+ registerWindowProvider(value: WindowProviderInterface): void;
+
+ getImageFinder(): ImageFinderInterface;
+
+ registerImageFinder(value: ImageFinderInterface): void;
+
+ getImageReader(): ImageReader;
+
+ registerImageReader(value: ImageReader): void;
+
+ getImageWriter(): ImageWriter;
+
+ registerImageWriter(value: ImageWriter): void;
+
+ getImageProcessor(): ImageProcessor;
+
+ registerImageProcessor(value: ImageProcessor): void;
+}
+
+class DefaultProviderRegistry implements ProviderRegistry {
+ private _clipboard?: ClipboardProviderInterface;
+ private _finder?: ImageFinderInterface;
+ private _keyboard?: KeyboardProviderInterface;
+ private _mouse?: MouseProviderInterface;
+ private _screen?: ScreenProviderInterface;
+ private _window?: WindowProviderInterface;
+ private _imageReader?: ImageReader;
+ private _imageWriter?: ImageWriter;
+ private _imageProcessor?: ImageProcessor;
+
+ getClipboard(): ClipboardProviderInterface {
+ if (this._clipboard) {
+ return this._clipboard;
+ }
+ throw new Error(`No ClipboardProvider registered`);
+ }
+
+ registerClipboardProvider(value: ClipboardProviderInterface) {
+ this._clipboard = value;
+ }
+
+ getImageFinder(): ImageFinderInterface {
+ if (this._finder) {
+ return this._finder;
+ }
+ throw new Error(`No ImageFinder registered`);
+ }
+
+ registerImageFinder(value: ImageFinderInterface) {
+ this._finder = value;
+ }
+
+ getKeyboard(): KeyboardProviderInterface {
+ if (this._keyboard) {
+ return this._keyboard;
+ }
+ throw new Error(`No KeyboardProvider registered`);
+ }
+
+ registerKeyboardProvider(value: KeyboardProviderInterface) {
+ this._keyboard = value;
+ }
+
+ getMouse(): MouseProviderInterface {
+ if (this._mouse) {
+ return this._mouse;
+ }
+ throw new Error(`No MouseProvider registered`);
+ }
+
+ registerMouseProvider(value: MouseProviderInterface) {
+ this._mouse = value;
+ }
+
+ getScreen(): ScreenProviderInterface {
+ if (this._screen) {
+ return this._screen;
+ }
+ throw new Error(`No ScreenProvider registered`);
+ }
+
+ registerScreenProvider(value: ScreenProviderInterface) {
+ this._screen = value;
+ }
+
+ getWindow(): WindowProviderInterface {
+ if (this._window) {
+ return this._window;
+ }
+ throw new Error(`No WindowProvider registered`);
+ }
+
+ registerWindowProvider(value: WindowProviderInterface) {
+ this._window = value;
+ }
+
+ getImageReader(): ImageReader {
+ if (this._imageReader) {
+ return this._imageReader;
+ }
+ throw new Error(`No ImageReader registered`);
+ }
+
+ registerImageReader(value: ImageReader) {
+ this._imageReader = value;
+ }
+
+ getImageWriter(): ImageWriter {
+ if (this._imageWriter) {
+ return this._imageWriter;
+ }
+ throw new Error(`No ImageWriter registered`);
+ }
+
+ registerImageWriter(value: ImageWriter) {
+ this._imageWriter = value;
+ }
+
+ getImageProcessor(): ImageProcessor {
+ if (this._imageProcessor) {
+ return this._imageProcessor;
+ }
+ throw new Error(`No ImageProcessor registered`);
+ }
+
+ registerImageProcessor(value: ImageProcessor): void {
+ this._imageProcessor = value;
+ }
+}
+
+const providerRegistry = new DefaultProviderRegistry();
+
+providerRegistry.registerClipboardProvider(new Clipboard());
+providerRegistry.registerKeyboardProvider(new Keyboard());
+providerRegistry.registerMouseProvider(new Mouse());
+providerRegistry.registerScreenProvider(new Screen());
+providerRegistry.registerWindowProvider(new Window());
+providerRegistry.registerImageWriter(new ImageWriterImpl());
+providerRegistry.registerImageReader(new ImageReaderImpl());
+providerRegistry.registerImageProcessor(new ImageProcessorImpl());
+
+export default providerRegistry;
\ No newline at end of file
diff --git a/lib/provider/native/screen-action-provider.interface.ts b/lib/provider/screen-provider.interface.ts
similarity index 89%
rename from lib/provider/native/screen-action-provider.interface.ts
rename to lib/provider/screen-provider.interface.ts
index 2eb390df..367ad65f 100644
--- a/lib/provider/native/screen-action-provider.interface.ts
+++ b/lib/provider/screen-provider.interface.ts
@@ -1,12 +1,12 @@
-import { Image } from "../../image.class";
-import { Region } from "../../region.class";
+import { Image } from "../image.class";
+import { Region } from "../region.class";
/**
* A ScreenActionProvider should provide access to a system's main screen
*
- * @interface ScreenActionProvider
+ * @interface ScreenProviderInterface
*/
-export interface ScreenActionProvider {
+export interface ScreenProviderInterface {
/**
* grabScreen should return an {@link Image} object containing a screenshot data of a systems
* main screen as well as its dimensions
diff --git a/lib/provider/native/window-action-provider.interface.ts b/lib/provider/window-provider.interface.ts
similarity index 90%
rename from lib/provider/native/window-action-provider.interface.ts
rename to lib/provider/window-provider.interface.ts
index 0d5e3e42..f6e4880d 100644
--- a/lib/provider/native/window-action-provider.interface.ts
+++ b/lib/provider/window-provider.interface.ts
@@ -1,11 +1,11 @@
-import { Region } from "../../region.class";
+import { Region } from "../region.class";
/**
* A WindowActionProvider should provide access to a system's window system
*
- * @interface WindowActionProvider
+ * @interface WindowProviderInterface
*/
-export interface WindowActionProvider {
+export interface WindowProviderInterface {
/**
* {@link getWindows} returns a list of window handles for further processing.
* These window handles may serve as input to e.g. {@link getWindowTitle}
diff --git a/lib/region.class.spec.ts b/lib/region.class.spec.ts
index df1c0b64..077f46d1 100644
--- a/lib/region.class.spec.ts
+++ b/lib/region.class.spec.ts
@@ -1,7 +1,7 @@
import { Region } from "./region.class";
describe("Region", () => {
- it("should calcutate the correct area of a region", () => {
+ it("should calculate the correct area of a region", () => {
const region = new Region(0, 0, 100, 100);
const expected = 10000;
@@ -14,56 +14,4 @@ describe("Region", () => {
expect(region.toString()).toEqual(expected);
});
-
- it("should scale and translate in x", () => {
- const scaleFactor = 2.0;
- const region = new Region(100, 100, 100, 100);
- const result = Region.scaled(region, scaleFactor);
-
- expect(result.left).toBeCloseTo(region.left * scaleFactor);
- expect(result.width).toBeCloseTo(region.width * scaleFactor);
- expect(result.top).toBeCloseTo(region.top);
- expect(result.height).toBeCloseTo(region.height);
- });
-
- it("should scale and translate in y", () => {
- const scaleFactor = 2.0;
- const region = new Region(200, 250, 100, 100);
- const result = Region.scaled(region, undefined, scaleFactor);
-
- expect(result.left).toBeCloseTo(region.left);
- expect(result.width).toBeCloseTo(region.width);
- expect(result.top).toBeCloseTo(region.top * scaleFactor);
- expect(result.height).toBeCloseTo(region.height * scaleFactor);
- });
-
- it("should scale and translate in both x and y", () => {
- const scaleFactorX = 1.75;
- const scaleFactorY = 2.5;
- const region = new Region(300, 720, 100, 100);
- const result = Region.scaled(region, scaleFactorX, scaleFactorY);
-
- expect(result.left).toBeCloseTo(region.left * scaleFactorX);
- expect(result.width).toBeCloseTo(region.width * scaleFactorX);
- expect(result.top).toBeCloseTo(region.top * scaleFactorY);
- expect(result.height).toBeCloseTo(region.height * scaleFactorY);
- });
-
- it("should throw an error when scaling to 0 in x", () => {
- const scaleFactorX = 0.0;
- const scaleFactorY = 2.5;
- const region = new Region(300, 720, 100, 100);
- expect(() => Region.scaled(region, scaleFactorX, scaleFactorY)).toThrow(
- `Scaling to 0. Please check parameters: scaleX: ${scaleFactorX}, scaleY: ${scaleFactorY}`,
- );
- });
-
- it("should throw an error when scaling to 0 in y", () => {
- const scaleFactorX = 2.5;
- const scaleFactorY = 0.0;
- const region = new Region(300, 720, 100, 100);
- expect(() => Region.scaled(region, scaleFactorX, scaleFactorY)).toThrow(
- `Scaling to 0. Please check parameters: scaleX: ${scaleFactorX}, scaleY: ${scaleFactorY}`,
- );
- });
});
diff --git a/lib/region.class.ts b/lib/region.class.ts
index a3e22c71..8dd15f4a 100644
--- a/lib/region.class.ts
+++ b/lib/region.class.ts
@@ -1,22 +1,4 @@
export class Region {
- public static scaled(
- region: Region,
- scaleX: number = 1.0,
- scaleY: number = 1.0,
- ): Region {
- if (scaleX === 0 || scaleY === 0) {
- throw new Error(
- `Scaling to 0. Please check parameters: scaleX: ${scaleX}, scaleY: ${scaleY}`,
- );
- }
- return new Region(
- region.left * scaleX,
- region.top * scaleY,
- region.width * scaleX,
- region.height * scaleY,
- );
- }
-
constructor(
public left: number,
public top: number,
@@ -29,7 +11,6 @@ export class Region {
}
public toString() {
- return `(${this.left}, ${this.top}, ${this.left + this.width}, ${this.top +
- this.height})`;
+ return `(${this.left}, ${this.top}, ${this.width}, ${this.height})`;
}
}
diff --git a/lib/rgba.class.ts b/lib/rgba.class.ts
new file mode 100644
index 00000000..9e40a245
--- /dev/null
+++ b/lib/rgba.class.ts
@@ -0,0 +1,13 @@
+export class RGBA {
+ constructor(public readonly R: number, public readonly G: number, public readonly B: number, public readonly A: number) {
+ }
+
+ public toString(): string {
+ return `rgb(${this.R},${this.G},${this.B})`;
+ }
+
+ public toHex(): string {
+ return `#${this.R.toString(16).padStart(2, '0')}${this.G.toString(16).padStart(2, '0')}${this.B.toString(16).padStart(2, '0')}${this.A.toString(16).padStart(2, '0')}`
+ }
+}
+
diff --git a/lib/screen.class.e2e.spec.ts b/lib/screen.class.e2e.spec.ts
deleted file mode 100644
index d575c9cc..00000000
--- a/lib/screen.class.e2e.spec.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import {existsSync} from "fs";
-import {VisionAdapter} from "./adapter/vision.adapter.class";
-import {FileType} from "./file-type.enum";
-import {Screen} from "./screen.class";
-import {sleep} from "./sleep.function";
-import AbortController from "node-abort-controller";
-
-describe("Screen.", () => {
- it("should capture the screen", () => {
- // GIVEN
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
-
- // WHEN
- SUT.capture("asdf", FileType.PNG).then(filename => {
- // THEN
- expect(filename).not.toBeNull();
- sleep(1000).then(() => {
- expect(existsSync(filename)).toBeTruthy();
- });
- });
- });
-
- it("should capture the screen and save to JPG", () => {
- // GIVEN
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
-
- // WHEN
- SUT.capture("asdf", FileType.JPG).then(filename => {
- // THEN
- expect(filename).not.toBeNull();
- sleep(1000).then(() => {
- expect(existsSync(filename)).toBeTruthy();
- });
- });
- });
-
- it("should capture the screen and save file with prefix", () => {
- // GIVEN
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
- const prefix = "foo_";
-
- // WHEN
- SUT.capture("asdf", FileType.JPG, "./", prefix).then(filename => {
- // THEN
- expect(filename.includes(prefix)).toBeTruthy();
- expect(filename).not.toBeNull();
- sleep(1000).then(() => {
- expect(existsSync(filename)).toBeTruthy();
- });
- });
- });
-
- it("should capture the screen and save file with postfix", () => {
- // GIVEN
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
- const postfix = "_bar";
-
- // WHEN
- SUT.capture("asdf", FileType.JPG, "./", "", postfix).then(filename => {
- // THEN
- expect(filename.includes(postfix)).toBeTruthy();
- expect(filename).not.toBeNull();
- sleep(1000).then(() => {
- expect(existsSync(filename)).toBeTruthy();
- });
- });
- });
-
- it("should capture the screen and save file with pre- and postfix", () => {
- // GIVEN
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
- const filename = "asdf";
- const prefix = "foo_";
- const postfix = "_bar";
-
- // WHEN
- SUT.capture("asdf", FileType.JPG, "./", prefix, postfix).then(output => {
- // THEN
- expect(output.includes(`${prefix}${filename}${postfix}`)).toBeTruthy();
- expect(output).not.toBeNull();
- sleep(1000).then(() => {
- expect(existsSync(output)).toBeTruthy();
- });
- });
- });
-
- it("should reject after timeout", async () => {
- // GIVEN
- jest.setTimeout(10000);
- const timeout = 5000;
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
- SUT.config.resourceDirectory = "./e2e/assets";
-
- // WHEN
- const start = Date.now();
- try {
- await SUT.waitFor("calculator.png", timeout);
- } catch (e) {
- // THEN
- expect(e).toBe(`Action timed out after ${timeout} ms`);
- }
- const end = Date.now();
-
- // THEN
- expect(end - start).toBeGreaterThanOrEqual(timeout);
- });
-
- it("should abort via signal", (done) => {
- // GIVEN
- jest.setTimeout(10000);
- const timeout = 5000;
- const abortAfterMs = 1000;
- const controller = new AbortController();
- const signal = controller.signal;
- const visionAdapter = new VisionAdapter();
- const SUT = new Screen(visionAdapter);
- SUT.config.resourceDirectory = "./e2e/assets";
-
- // WHEN
- const start = Date.now();
- SUT.waitFor("calculator.png", timeout, {abort: signal}).catch(e => {
- const end = Date.now();
-
- // THEN
- expect(e).toBe(`Action aborted by signal`);
- expect(end - start).toBeGreaterThanOrEqual(abortAfterMs);
- expect(end - start).toBeLessThan(timeout);
- done();
- });
- setTimeout(() => controller.abort(), abortAfterMs);
- });
-});
diff --git a/lib/screen.class.spec.ts b/lib/screen.class.spec.ts
index 29e735f4..0c162d94 100644
--- a/lib/screen.class.spec.ts
+++ b/lib/screen.class.spec.ts
@@ -1,75 +1,79 @@
-import { join } from "path";
-import { cwd } from "process";
-import { VisionAdapter } from "./adapter/vision.adapter.class";
-import { Image } from "./image.class";
-import { LocationParameters } from "./locationparameters.class";
-import { MatchRequest } from "./match-request.class";
-import { MatchResult } from "./match-result.class";
-import { Region } from "./region.class";
-import { Screen } from "./screen.class";
-import { mockPartial } from "sneer";
-import { FileType } from "./file-type.enum";
-
-jest.mock("./adapter/native.adapter.class");
-jest.mock("./adapter/vision.adapter.class");
+import {join} from "path";
+import {cwd} from "process";
+import {Image} from "./image.class";
+import {MatchRequest} from "./match-request.class";
+import {MatchResult} from "./match-result.class";
+import {Region} from "./region.class";
+import {ScreenClass} from "./screen.class";
+import {mockPartial} from "sneer";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {ImageFinderInterface, ImageWriter, ImageWriterParameters, ScreenProviderInterface} from "./provider";
+import {OptionalSearchParameters} from "./optionalsearchparameters.class";
+
+jest.mock('jimp', () => {
+});
const searchRegion = new Region(0, 0, 1000, 1000);
-beforeAll(() => {
- VisionAdapter.prototype.grabScreen = jest.fn(() => {
- return Promise.resolve(new Image(searchRegion.width, searchRegion.height, new ArrayBuffer(0), 3));
- });
+const providerRegistryMock = mockPartial({
+ getScreen(): ScreenProviderInterface {
+ return mockPartial({
+ grabScreenRegion(): Promise {
+ return Promise.resolve(new Image(searchRegion.width, searchRegion.height, Buffer.from([]), 3, "needle_image"));
+ },
+ screenSize(): Promise {
+ return Promise.resolve(searchRegion);
+ }
+ })
+ }
+});
- VisionAdapter.prototype.screenSize = jest.fn(() => {
- return Promise.resolve(searchRegion);
- });
+beforeEach(() => {
+ jest.resetAllMocks();
});
describe("Screen.", () => {
-
describe("find", () => {
it("should resolve with sufficient confidence.", async () => {
-
// GIVEN
const matchResult = new MatchResult(0.99, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const needlePromise = Promise.resolve(needle);
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
// WHEN
- const resultRegion = SUT.find(imagePath);
+ const resultRegion = SUT.find(needlePromise);
// THEN
await expect(resultRegion).resolves.toEqual(matchResult.location);
const matchRequest = new MatchRequest(
expect.any(Image),
- join(cwd(), imagePath),
- searchRegion,
+ needle,
SUT.config.confidence,
true);
- expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(matchRequest);
+ expect(findMatchMock).toHaveBeenCalledWith(matchRequest);
});
it("should call registered hook before resolve", async () => {
-
// GIVEN
const matchResult = new MatchResult(0.99, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
- const SUT = new Screen(visionAdapterMock);
+ const SUT = new ScreenClass(providerRegistryMock);
const testCallback = jest.fn(() => Promise.resolve());
- const imagePath = "test/path/to/image.png";
- SUT.on(imagePath, testCallback);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ SUT.on(needle, testCallback);
// WHEN
- await SUT.find(imagePath);
+ await SUT.find(needle);
// THEN
expect(testCallback).toBeCalledTimes(1);
@@ -77,23 +81,22 @@ describe("Screen.", () => {
});
it("should call multiple registered hooks before resolve", async () => {
-
// GIVEN
const matchResult = new MatchResult(0.99, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
- const SUT = new Screen(visionAdapterMock);
+ const SUT = new ScreenClass(providerRegistryMock);
const testCallback = jest.fn(() => Promise.resolve());
const secondCallback = jest.fn(() => Promise.resolve());
- const imagePath = "test/path/to/image.png";
- SUT.on(imagePath, testCallback);
- SUT.on(imagePath, secondCallback);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ SUT.on(needle, testCallback);
+ SUT.on(needle, secondCallback);
// WHEN
- await SUT.find(imagePath);
+ await SUT.find(needle);
// THEN
for (const callback of [testCallback, secondCallback]) {
@@ -105,48 +108,47 @@ describe("Screen.", () => {
it("should reject with insufficient confidence.", async () => {
// GIVEN
- const matchResult = new MatchResult(0.8, searchRegion);
-
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
-
- const visionAdapterMock = new VisionAdapter();
-
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
+ const minConfidence = 0.95;
+ const failingConfidence = 0.8;
+ const expectedReason = `No match with required confidence ${minConfidence}. Best match: ${failingConfidence}`;
+ const findMatchMock = jest.fn(() => Promise.reject(expectedReason));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const id = "needle_image";
+ const needle = new Image(100, 100, Buffer.from([]), 3, id);
// WHEN
- const resultRegion = SUT.find(imagePath);
+ const resultRegion = SUT.find(needle, {confidence: minConfidence});
// THEN
await expect(resultRegion)
.rejects
- .toEqual(`No match for ${imagePath}. Required: ${SUT.config.confidence}, given: ${matchResult.confidence}`);
+ .toEqual(`Searching for ${id} failed. Reason: '${expectedReason}'`);
});
it("should reject when search fails.", async () => {
// GIVEN
const rejectionReason = "Search failed.";
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.reject(rejectionReason);
- });
+ const findMatchMock = jest.fn(() => Promise.reject(rejectionReason));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
- const visionAdapterMock = new VisionAdapter();
-
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
+ const SUT = new ScreenClass(providerRegistryMock);
+ const id = "needle_image";
+ const needle = new Image(100, 100, Buffer.from([]), 3, id);
// WHEN
- const resultRegion = SUT.find(imagePath);
+ const resultRegion = SUT.find(needle);
// THEN
await expect(resultRegion)
.rejects
- .toEqual(`Searching for ${imagePath} failed. Reason: '${rejectionReason}'`);
-
-
+ .toEqual(`Searching for ${id} failed. Reason: '${rejectionReason}'`);
});
it("should override default confidence value with parameter.", async () => {
@@ -155,78 +157,79 @@ describe("Screen.", () => {
const minMatch = 0.8;
const matchResult = new MatchResult(minMatch, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
-
- const visionAdapterMock = new VisionAdapter();
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
- const SUT = new Screen(visionAdapterMock);
+ const SUT = new ScreenClass(providerRegistryMock);
- const imagePath = "test/path/to/image.png";
- const parameters = new LocationParameters(undefined, minMatch);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(undefined, minMatch);
// WHEN
- const resultRegion = SUT.find(imagePath, parameters);
+ const resultRegion = SUT.find(needle, parameters);
// THEN
await expect(resultRegion).resolves.toEqual(matchResult.location);
const matchRequest = new MatchRequest(
expect.any(Image),
- join(cwd(), imagePath),
- searchRegion,
+ needle,
minMatch,
true);
- expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(matchRequest);
+ expect(findMatchMock).toHaveBeenCalledWith(matchRequest);
});
it("should override default search region with parameter.", async () => {
// GIVEN
const customSearchRegion = new Region(10, 10, 90, 90);
const matchResult = new MatchResult(0.99, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
- const parameters = new LocationParameters(customSearchRegion);
+
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(customSearchRegion);
const expectedMatchRequest = new MatchRequest(
expect.any(Image),
- join(cwd(), imagePath),
- customSearchRegion,
+ needle,
SUT.config.confidence,
true);
// WHEN
- await SUT.find(imagePath, parameters);
+ await SUT.find(needle, parameters);
// THEN
- expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest);
+ expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest);
});
it("should override searchMultipleScales with parameter.", async () => {
// GIVEN
const matchResult = new MatchResult(0.99, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
- const parameters = new LocationParameters(searchRegion, undefined, false);
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+
+ const parameters = new OptionalSearchParameters(searchRegion, undefined, false);
const expectedMatchRequest = new MatchRequest(
expect.any(Image),
- join(cwd(), imagePath),
- searchRegion,
+ needle,
SUT.config.confidence,
false);
// WHEN
- await SUT.find(imagePath, parameters);
+ await SUT.find(needle, parameters);
// THEN
- expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest);
+ expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest);
});
it("should override both confidence and search region with parameter.", async () => {
@@ -234,29 +237,28 @@ describe("Screen.", () => {
const minMatch = 0.8;
const customSearchRegion = new Region(10, 10, 90, 90);
const matchResult = new MatchResult(minMatch, searchRegion);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imagePath = "test/path/to/image.png";
- const parameters = new LocationParameters(customSearchRegion, minMatch);
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(customSearchRegion, minMatch);
const expectedMatchRequest = new MatchRequest(
expect.any(Image),
- join(cwd(), imagePath),
- customSearchRegion,
+ needle,
minMatch,
true);
// WHEN
- await SUT.find(imagePath, parameters);
+ await SUT.find(needle, parameters);
// THEN
- expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest);
+ expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest);
});
it("should add search region offset to result image location", async () => {
-
// GIVEN
const limitedSearchRegion = new Region(100, 200, 300, 400);
const resultRegion = new Region(50, 100, 150, 200);
@@ -268,17 +270,19 @@ describe("Screen.", () => {
resultRegion.width,
resultRegion.height);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
- const SUT = new Screen(new VisionAdapter());
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+ const SUT = new ScreenClass(providerRegistryMock);
// WHEN
const matchRegion = await SUT.find(
- "test/path/to/image.png",
+ new Image(100, 100, Buffer.from([]), 3, "needle_image"),
{
searchRegion: limitedSearchRegion
- });
+ }
+ );
// THEN
expect(matchRegion).toEqual(expectedMatchRegion);
@@ -301,39 +305,287 @@ describe("Screen.", () => {
["with NaN y coordinate", new Region(0, "a" as unknown as number, 100, 600)],
["with NaN on width", new Region(0, 0, "a" as unknown as number, 100)],
["with NaN on height", new Region(0, 0, 100, "a" as unknown as number)],
- ])("should reject search regions %s", async (_, region) =>{
+ ])("should reject search regions %s", async (_, region) => {
+ // GIVEN
+ const id = "needle_image";
+ const needle = new Image(100, 100, Buffer.from([]), 3, id);
+ const matchResult = new MatchResult(0.99, region);
+ const findMatchMock = jest.fn(() => Promise.resolve(matchResult));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatch: findMatchMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+
+ // WHEN
+ const findPromise = SUT.find(
+ needle,
+ {
+ searchRegion: region
+ });
+
+ // THEN
+ await expect(findPromise).rejects.toContain(`Searching for ${id} failed. Reason:`);
+ })
+ });
+
+ describe("findAll", () => {
+ it("should call registered hook before resolve", async () => {
+ // GIVEN
+ const matchResult = new MatchResult(0.99, searchRegion);
+ const matchResults = [matchResult, matchResult, matchResult];
+ const findMatchesMock = jest.fn(() => Promise.resolve(matchResults));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const testCallback = jest.fn(() => Promise.resolve());
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ SUT.on(needle, testCallback);
+
+ // WHEN
+ await SUT.findAll(needle);
+
+ // THEN
+ expect(testCallback).toBeCalledTimes(matchResults.length);
+ expect(testCallback).toBeCalledWith(matchResult);
+ });
+
+ it("should call multiple registered hooks before resolve", async () => {
+ // GIVEN
+ const matchResult = new MatchResult(0.99, searchRegion);
+ const matchResults = [matchResult, matchResult, matchResult];
+ const findMatchesMock = jest.fn(() => Promise.resolve(matchResults));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const testCallback = jest.fn(() => Promise.resolve());
+ const secondCallback = jest.fn(() => Promise.resolve());
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ SUT.on(needle, testCallback);
+ SUT.on(needle, secondCallback);
+
+ // WHEN
+ await SUT.findAll(needle);
+
+ // THEN
+ for (const callback of [testCallback, secondCallback]) {
+ expect(callback).toBeCalledTimes(matchResults.length);
+ expect(callback).toBeCalledWith(matchResult);
+ }
+ });
+
+ it("should reject when search fails.", async () => {
+
+ // GIVEN
+ const rejectionReason = "Search failed.";
+ const findMatchesMock = jest.fn(() => Promise.reject(rejectionReason));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const id = "needle_image";
+ const needle = new Image(100, 100, Buffer.from([]), 3, id);
+
+ // WHEN
+ const resultRegion = SUT.findAll(needle);
+
+ // THEN
+ await expect(resultRegion)
+ .rejects
+ .toEqual(`Searching for ${id} failed. Reason: '${rejectionReason}'`);
+ });
+
+ it("should override default confidence value with parameter.", async () => {
+ // GIVEN
+ const minMatch = 0.8;
+ const matchResult = new MatchResult(minMatch, searchRegion);
+
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(undefined, minMatch);
+
+ // WHEN
+ const [resultRegion] = await SUT.findAll(needle, parameters);
+
+ // THEN
+ expect(resultRegion).toEqual(matchResult.location);
+ const matchRequest = new MatchRequest(
+ expect.any(Image),
+ needle,
+ minMatch,
+ true);
+ expect(findMatchesMock).toHaveBeenCalledWith(matchRequest);
+ });
+ it("should override default search region with parameter.", async () => {
// GIVEN
- const imagePath = "test/path/to/image.png"
- const visionAdapterMock = new VisionAdapter();
+ const customSearchRegion = new Region(10, 10, 90, 90);
+ const matchResult = new MatchResult(0.99, searchRegion);
+
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
- const SUT = new Screen(visionAdapterMock);
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(customSearchRegion);
+ const expectedMatchRequest = new MatchRequest(
+ expect.any(Image),
+ needle,
+ SUT.config.confidence,
+ true);
+
+ // WHEN
+ await SUT.findAll(needle, parameters);
+
+ // THEN
+ expect(findMatchesMock).toHaveBeenCalledWith(expectedMatchRequest);
+ });
+
+ it("should override searchMultipleScales with parameter.", async () => {
+ // GIVEN
+ const matchResult = new MatchResult(0.99, searchRegion);
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+
+ const parameters = new OptionalSearchParameters(searchRegion, undefined, false);
+ const expectedMatchRequest = new MatchRequest(
+ expect.any(Image),
+ needle,
+ SUT.config.confidence,
+ false);
+
+ // WHEN
+ await SUT.findAll(needle, parameters);
+
+ // THEN
+ expect(findMatchesMock).toHaveBeenCalledWith(expectedMatchRequest);
+ });
+
+ it("should override both confidence and search region with parameter.", async () => {
+ // GIVEN
+ const minMatch = 0.8;
+ const customSearchRegion = new Region(10, 10, 90, 90);
+ const matchResult = new MatchResult(minMatch, searchRegion);
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ const needle = new Image(100, 100, Buffer.from([]), 3, "needle_image");
+ const parameters = new OptionalSearchParameters(customSearchRegion, minMatch);
+ const expectedMatchRequest = new MatchRequest(
+ expect.any(Image),
+ needle,
+ minMatch,
+ true);
+
+ // WHEN
+ await SUT.findAll(needle, parameters);
+
+ // THEN
+ expect(findMatchesMock).toHaveBeenCalledWith(expectedMatchRequest);
+ });
+
+ it("should add search region offset to result image location", async () => {
+ // GIVEN
+ const limitedSearchRegion = new Region(100, 200, 300, 400);
+ const resultRegion = new Region(50, 100, 150, 200);
+ const matchResult = new MatchResult(0.99, resultRegion);
+
+ const expectedMatchRegion = new Region(
+ limitedSearchRegion.left + resultRegion.left,
+ limitedSearchRegion.top + resultRegion.top,
+ resultRegion.width,
+ resultRegion.height);
+
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
+ // WHEN
+ const [matchRegion] = await SUT.findAll(
+ new Image(100, 100, Buffer.from([]), 3, "needle_image"),
+ {
+ searchRegion: limitedSearchRegion
+ }
+ );
+
+ // THEN
+ expect(matchRegion).toEqual(expectedMatchRegion);
+ })
+
+ it.each([
+ ["with negative x coordinate", new Region(-1, 0, 100, 100)],
+ ["with negative y coordinate", new Region(0, -1, 100, 100)],
+ ["with negative width", new Region(0, 0, -100, 100)],
+ ["with negative height", new Region(0, 0, 100, -100)],
+ ["with region outside screen on x axis", new Region(1100, 0, 100, 100)],
+ ["with region outside screen on y axis", new Region(0, 1100, 100, 100)],
+ ["with region bigger than screen on x axis", new Region(0, 0, 1100, 100)],
+ ["with region bigger than screen on y axis", new Region(0, 0, 1000, 1100)],
+ ["with region of 1 px width", new Region(0, 0, 1, 1100)],
+ ["with region of 1 px height", new Region(0, 0, 100, 1)],
+ ["with region leaving screen on x axis", new Region(600, 0, 500, 100)],
+ ["with region leaving screen on y axis", new Region(0, 500, 100, 600)],
+ ["with NaN x coordinate", new Region("a" as unknown as number, 0, 100, 100)],
+ ["with NaN y coordinate", new Region(0, "a" as unknown as number, 100, 600)],
+ ["with NaN on width", new Region(0, 0, "a" as unknown as number, 100)],
+ ["with NaN on height", new Region(0, 0, 100, "a" as unknown as number)],
+ ])("should reject search regions %s", async (_, region) => {
+ // GIVEN
+ const id = "needle_image";
+ const needle = new Image(100, 100, Buffer.from([]), 3, id);
const matchResult = new MatchResult(0.99, region);
- VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => {
- return Promise.resolve(matchResult);
- });
+ const findMatchesMock = jest.fn(() => Promise.resolve([matchResult]));
+ providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({
+ findMatches: findMatchesMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
// WHEN
- const findPromise = SUT.find(
- imagePath,
+ const findPromise = SUT.findAll(
+ needle,
{
searchRegion: region
});
// THEN
- await expect(findPromise).rejects.toContain(`Searching for ${imagePath} failed. Reason:`);
+ await expect(findPromise).rejects.toContain(`Searching for ${id} failed. Reason:`);
})
});
-
it("should return region to highlight for chaining", async () => {
// GIVEN
const highlightRegion = new Region(10, 20, 30, 40);
- VisionAdapter.prototype.highlightScreenRegion = jest.fn();
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
+ const highlightMock = jest.fn((value: any) => Promise.resolve(value));
+ providerRegistryMock.getScreen = jest.fn(() => mockPartial({
+ highlightScreenRegion: highlightMock
+ }));
+ const SUT = new ScreenClass(providerRegistryMock);
// WHEN
const result = await SUT.highlight(highlightRegion);
@@ -345,9 +597,12 @@ describe("Screen.", () => {
// GIVEN
const highlightRegion = new Region(10, 20, 30, 40);
const highlightRegionPromise = new Promise(res => res(highlightRegion));
- VisionAdapter.prototype.highlightScreenRegion = jest.fn();
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
+ const highlightMock = jest.fn((value: any) => Promise.resolve(value));
+ providerRegistryMock.getScreen = jest.fn(() => mockPartial({
+ highlightScreenRegion: highlightMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
// WHEN
const result = await SUT.highlight(highlightRegionPromise);
@@ -356,85 +611,60 @@ describe("Screen.", () => {
expect(result).toEqual(highlightRegion);
});
- describe("capture",() => {
- it("should capture the whole screen and save image", async() => {
-
+ describe("capture", () => {
+ it("should capture the whole screen and save image", async () => {
// GIVEN
- const screenshot = mockPartial({data: "pretty pretty image"});
- VisionAdapter.prototype.grabScreen = jest.fn(() => Promise.resolve(screenshot));
- VisionAdapter.prototype.saveImage = jest.fn();
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
+ const screenshot = mockPartial({data: Buffer.from([])});
+ const grabScreenMock = jest.fn(() => Promise.resolve(screenshot));
+ const saveImageMock = jest.fn();
+ providerRegistryMock.getScreen = jest.fn(() => mockPartial({
+ grabScreen: grabScreenMock
+ }));
+ providerRegistryMock.getImageWriter = jest.fn(() => mockPartial({
+ store: saveImageMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
const imageName = "foobar.png"
const expectedImagePath = join(cwd(), imageName)
+ const expectedData: ImageWriterParameters = {image: screenshot, path: expectedImagePath}
// WHEN
const imagePath = await SUT.capture(imageName)
// THEN
expect(imagePath).toBe(expectedImagePath)
- expect(VisionAdapter.prototype.grabScreen).toHaveBeenCalled()
- expect(VisionAdapter.prototype.saveImage).toHaveBeenCalledWith(screenshot,expectedImagePath)
- })
-
- it("should consider output configuration", async () => {
-
- // GIVEN
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imageName = "foobar"
- const filePath = "/path/to/file"
- const prefix = "answer_"
- const postfix = "_42"
- const expectedImagePath = join(filePath, `${prefix}${imageName}${postfix}${FileType.JPG.toString()}`)
-
- // WHEN
- const imagePath = await SUT.capture(imageName, FileType.JPG, filePath, prefix, postfix)
-
- // THEN
- expect(imagePath).toBe(expectedImagePath)
- })
+ expect(grabScreenMock).toHaveBeenCalled()
+ expect(saveImageMock).toHaveBeenCalledWith(expectedData);
+ });
})
describe("captureRegion", () => {
-
it("should capture the specified region of the screen and save image", async () => {
// GIVEN
- const screenshot = mockPartial({data: "pretty partial image"});
- const regionToCapture = mockPartial({top:42, left:9, height: 10, width: 3.14159265359})
- VisionAdapter.prototype.grabScreenRegion = jest.fn(() => Promise.resolve(screenshot));
- VisionAdapter.prototype.saveImage = jest.fn();
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
+ const screenshot = mockPartial({data: Buffer.from([])});
+ const regionToCapture = mockPartial({top: 42, left: 9, height: 10, width: 3.14159265359})
+ const grabScreenMock = jest.fn(() => Promise.resolve(screenshot));
+ const saveImageMock = jest.fn();
+ providerRegistryMock.getScreen = jest.fn(() => mockPartial({
+ grabScreenRegion: grabScreenMock
+ }));
+ providerRegistryMock.getImageWriter = jest.fn(() => mockPartial({
+ store: saveImageMock
+ }));
+
+ const SUT = new ScreenClass(providerRegistryMock);
const imageName = "foobar.png"
const expectedImagePath = join(cwd(), imageName)
+ const expectedData: ImageWriterParameters = {image: screenshot, path: expectedImagePath}
// WHEN
const imagePath = await SUT.captureRegion(imageName, regionToCapture)
// THEN
expect(imagePath).toBe(expectedImagePath)
- expect(VisionAdapter.prototype.grabScreenRegion).toHaveBeenCalledWith(regionToCapture)
- expect(VisionAdapter.prototype.saveImage).toHaveBeenCalledWith(screenshot,expectedImagePath)
- })
-
- it("should consider output configuration", async () => {
-
- // GIVEN
- const regionToCapture = mockPartial({top:42, left:9, height: 10, width: 3.14159265359})
- const visionAdapterMock = new VisionAdapter();
- const SUT = new Screen(visionAdapterMock);
- const imageName = "foobar"
- const filePath = "/path/to/file"
- const prefix = "answer_"
- const postfix = "_42"
- const expectedImagePath = join(filePath, `${prefix}${imageName}${postfix}${FileType.JPG.toString()}`)
-
- // WHEN
- const imagePath = await SUT.captureRegion(imageName, regionToCapture, FileType.JPG, filePath, prefix, postfix)
-
- // THEN
- expect(imagePath).toBe(expectedImagePath)
- })
- })
+ expect(grabScreenMock).toHaveBeenCalledWith(regionToCapture)
+ expect(saveImageMock).toHaveBeenCalledWith(expectedData);
+ });
+ });
});
diff --git a/lib/screen.class.ts b/lib/screen.class.ts
index 1fe231a1..e095db9a 100644
--- a/lib/screen.class.ts
+++ b/lib/screen.class.ts
@@ -1,24 +1,40 @@
-import {join, normalize} from "path";
import {cwd} from "process";
-import {VisionAdapter} from "./adapter/vision.adapter.class";
import {FileType} from "./file-type.enum";
import {generateOutputPath} from "./generate-output-path.function";
-import {LocationParameters} from "./locationparameters.class";
import {MatchRequest} from "./match-request.class";
import {MatchResult} from "./match-result.class";
import {Region} from "./region.class";
import {timeout} from "./util/timeout.function";
import {Image} from "./image.class";
+import {ProviderRegistry} from "./provider/provider-registry.class";
+import {FirstArgumentType} from "./typings";
+import {Point} from "./point.class";
+import {OptionalSearchParameters} from "./optionalsearchparameters.class";
export type FindHookCallback = (target: MatchResult) => Promise;
+function validateSearchRegion(search: Region, screen: Region) {
+ if (search.left < 0 || search.top < 0 || search.width < 0 || search.height < 0) {
+ throw new Error(`Negative values in search region ${search}`)
+ }
+ if (isNaN(search.left) || isNaN(search.top) || isNaN(search.width) || isNaN(search.height)) {
+ throw new Error(`NaN values in search region ${search}`)
+ }
+ if (search.width < 2 || search.height < 2) {
+ throw new Error(`Search region ${search} is not large enough. Must be at least two pixels in both width and height.`)
+ }
+ if (search.left + search.width > screen.width || search.top + search.height > screen.height) {
+ throw new Error(`Search region ${search} extends beyond screen boundaries (${screen.width}x${screen.height})`)
+ }
+}
+
/**
- * {@link Screen} class provides methods to access screen content of a systems main display
+ * {@link ScreenClass} class provides methods to access screen content of a systems main display
*/
-export class Screen {
+export class ScreenClass {
/**
- * Config object for {@link Screen} class
+ * Config object for {@link ScreenClass} class
*/
public config = {
/**
@@ -47,13 +63,13 @@ export class Screen {
};
/**
- * {@link Screen} class constructor
- * @param vision {@link VisionAdapter} instance which bundles access to screen and / or computer vision related APIs
- * @param findHooks A {@link Map} of {@link FindHookCallback} methods assigned to a template image filename
+ * {@link ScreenClass} class constructor
+ * @param providerRegistry A {@link ProviderRegistry} used to access underlying implementations
+ * @param findHooks A {@link Map} of {@link FindHookCallback} methods assigned to a template image
*/
constructor(
- private vision: VisionAdapter,
- private findHooks: Map = new Map()) {
+ private providerRegistry: ProviderRegistry,
+ private findHooks: Map = new Map