diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 50a401b1..f64003b8 100755 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -8,6 +8,7 @@ about: Create a bug report to help us enhance our images **Short overview** **Issue occurs on** + - [ ] Virtual machine - [ ] Docker container - [ ] Dev/Host system @@ -17,4 +18,5 @@ about: Create a bug report to help us enhance our images **Steps to reproduce error** **Additional content** -> Please provide any (mandatory) additional data to reproduce the error (Dockerfiles etc.) \ No newline at end of file + +> Please provide any (mandatory) additional data to reproduce the error (Dockerfiles etc.) diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 7fc1723c..58244189 100755 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -10,4 +10,5 @@ about: Suggest a possible enhancement to the project **Detailed description** **Additional content** + > Please provide any (mandatory) additional data for your enhancement diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index 5c64de43..362bec4c 100755 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,6 +1,6 @@ --- name: Feature request -about: Open a feature request +about: Open a feature request --- **Short overview** @@ -10,4 +10,5 @@ about: Open a feature request **Detailed feature description** **Additional content** -> Please provide any (mandatory) additional data for your desired feature \ No newline at end of file + +> Please provide any (mandatory) additional data for your desired feature diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 76b9c562..28b0e3fb 100755 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -6,9 +6,9 @@ about: File a request to resolve open questions **Short summary** **Desired execution environment / tested on** + - [ ] Virtual machine - [ ] Docker container - [ ] Dev/Host system - -**Detailed question** \ No newline at end of file +**Detailed question** diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md index bef0d185..acab9a72 100755 --- a/.github/PULL_REQUEST_TEMPLATE/bugfix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md @@ -12,4 +12,4 @@ about: Request to merge a bugfix **Checklist** - [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. \ No newline at end of file +- [ ] I have updated the documentation accordingly. diff --git a/.github/PULL_REQUEST_TEMPLATE/enhancement.md b/.github/PULL_REQUEST_TEMPLATE/enhancement.md index 9f4128a4..900d3e43 100755 --- a/.github/PULL_REQUEST_TEMPLATE/enhancement.md +++ b/.github/PULL_REQUEST_TEMPLATE/enhancement.md @@ -12,4 +12,4 @@ about: Request to merge an enhancement **Checklist** - [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. \ No newline at end of file +- [ ] I have updated the documentation accordingly. diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md index f30068ca..9739cd4a 100755 --- a/.github/PULL_REQUEST_TEMPLATE/feature.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -1,6 +1,6 @@ --- name: Feature -about: Request to merge a new feature +about: Request to merge a new feature --- **Short summary** @@ -12,4 +12,4 @@ about: Request to merge a new feature **Checklist** - [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. \ No newline at end of file +- [ ] I have updated the documentation accordingly. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index db62dcb3..315917b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,17 +1,17 @@ name: Run CI on: -# push: -# branches-ignore: -# - develop -# - release/** -# paths-ignore: -# - '**/*.md' + # push: + # branches-ignore: + # - develop + # - release/** + # paths-ignore: + # - '**/*.md' pull_request: jobs: sonar: runs-on: ubuntu-latest - if: '!contains(github.event.head_commit.message, ''skip ci'')' + if: "!contains(github.event.head_commit.message, 'skip ci')" steps: - name: Set up Git repository uses: actions/checkout@v2 @@ -56,8 +56,8 @@ jobs: - sonar strategy: matrix: - os: [ windows-latest, macos-latest ] - node: [ 16 ] + os: [windows-latest, macos-latest] + node: [16] 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 1d797a87..a8ccbd52 100644 --- a/.github/workflows/snapshot_release.yaml +++ b/.github/workflows/snapshot_release.yaml @@ -4,7 +4,7 @@ on: branches: - develop paths-ignore: - - '**/*.md' + - "**/*.md" repository_dispatch: types: - snapshot-release @@ -13,8 +13,8 @@ jobs: test: strategy: matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - node: [ 16 ] + os: [ubuntu-latest, windows-latest, macos-latest] + node: [16] runs-on: ${{matrix.os}} steps: - name: Set up Git repository @@ -48,7 +48,6 @@ jobs: with: run: npm --prefix e2e/electron-test cit - deploy: needs: - test @@ -60,7 +59,7 @@ jobs: uses: actions/setup-node@v2 with: node-version: 16 - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install run: npm ci - name: Install @nut-tree/libnut@next @@ -73,7 +72,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: actions/setup-node@v2 with: - registry-url: 'https://npm.pkg.github.com' + registry-url: "https://npm.pkg.github.com" - name: Publish snapshot release to GPR run: npm run publish-next env: diff --git a/.github/workflows/tagged_release.yaml b/.github/workflows/tagged_release.yaml index 23281cdb..df8126ba 100644 --- a/.github/workflows/tagged_release.yaml +++ b/.github/workflows/tagged_release.yaml @@ -8,8 +8,8 @@ jobs: test: strategy: matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - node: [ 16 ] + os: [ubuntu-latest, windows-latest, macos-latest] + node: [16] runs-on: ${{matrix.os}} steps: - name: Set up Git repository @@ -52,7 +52,7 @@ jobs: uses: actions/setup-node@v2 with: node-version: 16 - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - name: Install run: npm ci - name: Run typedoc @@ -69,8 +69,8 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: actions/setup-node@v2 with: - registry-url: 'https://npm.pkg.github.com' + 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 + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..d24fdfc6 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..96e8983a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Ignore artifacts: +dist +docs +coverage +node_modules diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0d15ec..bc6a9987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,17 @@ All notable changes to this project will be documented in this file. ## 2.3.0 + - Bugfix: Segmentation Fault when retrieving window title [(#377)](https://github.com/nut-tree/nut.js/issues/377) - Enhancement: Automatically check and request required permissions on macOS [(#377)](https://github.com/nut-tree/nut.js/issues/377) ## 2.2.1 + - Enhancement: Scale easing function result by base speed before applying [(#425)](https://github.com/nut-tree/nut.js/issues/425) - Maintenance: Resolve security vulnerabilities [(#422)](https://github.com/nut-tree/nut.js/issues/422) ## 2.2.0 + - Maintenance: Limit CI runs to PRs, not every push - Maintenance: Upgrade node version to 16 for all CI runs - Bugfix: Fix grave accent [(PR #414)](https://github.com/nut-tree/nut.js/pull/414) @@ -18,10 +21,12 @@ All notable changes to this project will be documented in this file. - Enhancement: Ship Windows runtime dependencies [(#365)](https://github.com/nut-tree/nut.js/issues/365) ## 2.1.1 + - Bugfix: Modifier keys are not properly released on macOS [(#264)](https://github.com/nut-tree/nut.js/issues/264) - Bugfix: Fix mouse clicks with modifiers on macOS [(#273)](https://github.com/nut-tree/nut.js/issues/273) ## 2.1.0 + - Bugfix: Keyboard methods `pressKey` and `releaseKey` ignore updated autoDelayMs [(#188)](https://github.com/nut-tree/nut.js/issues/188) - Enhancement: Add mappings for missing numpad keys [(#367)](https://github.com/nut-tree/nut.js/issues/367) - Enhancement: macOS double click [(#373)](https://github.com/nut-tree/nut.js/issues/373) @@ -30,10 +35,12 @@ All notable changes to this project will be documented in this file. - Bugfix: Mouse methods `pressButton` and `releaseButton` should respect auto delay [(#403)](https://github.com/nut-tree/nut.js/issues/403) ## 2.0.1 + - Bugfix: Issue with `keyboard.type` in to Spotlight on MacOS [(#152)](https://github.com/nut-tree/nut.js/issues/152) - Enhancement: Numpad buttons don't work on Linux [(#360)](https://github.com/nut-tree/nut.js/issues/360) ## 2.0.0 + - Feature: Apple Silicon [(libnut#49)](https://github.com/nut-tree/libnut/issues/49) - Enhancement: Enable warning message for missing accessibility permissions on macOS [(#354)](https://github.com/nut-tree/nut.js/issues/354) - Enhancement: Add runtime typechecks for `screen.find` etc. [(#351)](https://github.com/nut-tree/nut.js/issues/351) @@ -62,14 +69,16 @@ All notable changes to this project will be documented in this file. - Feature: Add methods to grab the current screen content as Buffer [(#278)](https://github.com/nut-tree/nut.js/issues/278) ## 1.7.0 + - Enhancement: Trigger snapshot releases [(#234)](https://github.com/nut-tree/nut.js/issues/234) - Feature: Cancel screen.waitFor if needed [(#241)](https://github.com/nut-tree/nut.js/issues/241) - Enhancement: Move docs into separate repo [(#244)](https://github.com/nut-tree/nut.js/issues/244) - Feature: Support for node 16 and Electron 13 [(#246)](https://github.com/nut-tree/nut.js/issues/246) ## 1.6.0 + - Feature: Create screenshot from region [(#154)](https://github.com/nut-tree/nut.js/issues/154) -- Bugfix: Endless loop in timeout function for long-running actions returning undefined [(#205)](https://github.com/nut-tree/nut.js/issues/205) +- Bugfix: Endless loop in timeout function for long-running actions returning undefined [(#205)](https://github.com/nut-tree/nut.js/issues/205) - Maintenance: Use default exports for all provider classes [(#163)](https://github.com/nut-tree/nut.js/issues/163) - Enhancement: imprecise error message if image is too large [(#169)](https://github.com/nut-tree/nut.js/issues/169) - Bugfix: `waitFor` does not properly cancel [(#174)](https://github.com/nut-tree/nut.js/issues/174) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a2443044..753f9efe 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,5 @@ # Code of Conduct + All participants of nut.js are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with nut.js. ## Our Pledge @@ -15,22 +16,22 @@ We do not engage with nor tolerate any kind of extremism, political propaganda a Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/README.md b/README.md index ae786d8d..3e951671 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# nut.js (Native UI Toolkit) +# nut.js (Native UI Toolkit) -| |GitHub Actions| -|:-: |:-: | -|Master |![Create tagged release](https://github.com/nut-tree/nut.js/workflows/Create%20tagged%20release/badge.svg)| -|Develop|![Create snapshot release](https://github.com/nut-tree/nut.js/workflows/Create%20snapshot%20release/badge.svg)| +| | GitHub Actions | +| :-----: | :------------------------------------------------------------------------------------------------------------: | +| Master | ![Create tagged release](https://github.com/nut-tree/nut.js/workflows/Create%20tagged%20release/badge.svg) | +| Develop | ![Create snapshot release](https://github.com/nut-tree/nut.js/workflows/Create%20snapshot%20release/badge.svg) | [![SonarCloud badge](https://sonarcloud.io/api/project_badges/measure?project=nut-tree%3Anut.js&metric=alert_status)](https://sonarcloud.io/dashboard?id=nut-tree%3Anut.js) [![SonarCloud Coverage](https://sonarcloud.io/api/project_badges/measure?project=nut-tree%3Anut.js&metric=coverage)](https://sonarcloud.io/component_measures?id=nut-tree%3Anut.js&metric=coverage) @@ -48,7 +48,6 @@ A huge **"Thank you!"** goes out to all sponsors who make open source a bit more [Mighty browser logo](https://www.mightyapp.com) - # Demo Check out this demo video to get a first impression of what nut.js is capable of. @@ -121,7 +120,16 @@ The following snippet shows a valid `nut.js` example: ```js "use strict"; -const { mouse, left, right, up, down, straightTo, centerOf, Region} = require("@nut-tree/nut-js"); +const { + mouse, + left, + right, + up, + down, + straightTo, + centerOf, + Region, +} = require("@nut-tree/nut-js"); const square = async () => { await mouse.move(right(500)); @@ -131,14 +139,8 @@ const square = async () => { }; (async () => { - await square(); - await mouse.move( - straightTo( - centerOf( - new Region(100, 100, 200, 300) - ) - ) - ); + await square(); + await mouse.move(straightTo(centerOf(new Region(100, 100, 200, 300)))); })(); ``` @@ -156,6 +158,7 @@ In case you're running Windows 10 N and want to use [ImageFinder plugins](https: On macOS, Xcode command line tools are required. You can install them by running + ```bash xcode-select --install ``` @@ -197,6 +200,7 @@ In general, `nut.js` requires - libXtst Installation on `*buntu` distributions: + ```bash sudo apt-get install libxtst-dev ``` @@ -205,7 +209,7 @@ Setups on other distributions might differ. ## Install `nut.js` -Running +Running ```bash npm i @nut-tree/nut-js @@ -223,7 +227,7 @@ will install `nut.js` and its required dependencies. `nut.js` also provides snapshot releases which allows to test upcoming features. -Running +Running ```bash npm i @nut-tree/nut-js@next diff --git a/e2e/electron-test/index.css b/e2e/electron-test/index.css index 4b175247..118138dd 100644 --- a/e2e/electron-test/index.css +++ b/e2e/electron-test/index.css @@ -1,19 +1,19 @@ body { - width: 100vw; - height: 100vh; + width: 100vw; + height: 100vh; } #content { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 100vh; - width: 100vw; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; } #exit { - color: white; - font-size: 1.5rem; - background: darkblue; -} \ No newline at end of file + color: white; + font-size: 1.5rem; + background: darkblue; +} diff --git a/e2e/electron-test/index.html b/e2e/electron-test/index.html index 3dc08eb7..ff5ba54f 100644 --- a/e2e/electron-test/index.html +++ b/e2e/electron-test/index.html @@ -1,10 +1,13 @@ - + - - + + Hello from nut.js! diff --git a/e2e/electron-test/main.js b/e2e/electron-test/main.js index bff76b95..afd59a10 100644 --- a/e2e/electron-test/main.js +++ b/e2e/electron-test/main.js @@ -1,52 +1,56 @@ -const {app, ipcMain, BrowserWindow} = require('electron') -const {getActiveWindow} = require("@nut-tree/nut-js"); -const path = require('path') -const assert = require('assert'); +const { app, ipcMain, BrowserWindow } = require("electron"); +const { getActiveWindow } = require("@nut-tree/nut-js"); +const path = require("path"); +const assert = require("assert"); -const title = "nut.js Electron test" +const title = "nut.js Electron test"; function createWindow() { - const mainWindow = new BrowserWindow({ - width: 800, - height: 600, - title, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - preload: path.join(__dirname, 'preload.js') - } - }) - mainWindow.loadFile(path.join(__dirname, "index.html")) - mainWindow.maximize(); - - (async () => { - // GIVEN - const foregroundWindow = await getActiveWindow(); - - // WHEN - const windowTitle = await foregroundWindow.title; - - // THEN - assert.strictEqual(windowTitle, title, `Wrong foreground window. Expected ${title}, got ${windowTitle}`); - })(); + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + title, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + preload: path.join(__dirname, "preload.js"), + }, + }); + mainWindow.loadFile(path.join(__dirname, "index.html")); + mainWindow.maximize(); + + (async () => { + // GIVEN + const foregroundWindow = await getActiveWindow(); + + // WHEN + const windowTitle = await foregroundWindow.title; + + // THEN + assert.strictEqual( + windowTitle, + title, + `Wrong foreground window. Expected ${title}, got ${windowTitle}` + ); + })(); } ipcMain.on("main", (event, args) => { - if (args === "quit") { - app.quit(); - } + if (args === "quit") { + app.quit(); + } }); app.whenReady().then(() => { - setTimeout(() => process.exit(0), 15000); - createWindow() + setTimeout(() => process.exit(0), 15000); + createWindow(); - app.on('activate', function () { - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) -}) + app.on("activate", function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); -app.on('window-all-closed', function () { - console.log("Bye!"); - app.quit(); -}) +app.on("window-all-closed", function () { + console.log("Bye!"); + app.quit(); +}); diff --git a/e2e/electron-test/renderer.js b/e2e/electron-test/renderer.js index c3f88f49..be415018 100644 --- a/e2e/electron-test/renderer.js +++ b/e2e/electron-test/renderer.js @@ -1,6 +1,6 @@ -const {ipcRenderer} = require("electron"); +const { ipcRenderer } = require("electron"); -const close = document.getElementById('exit'); +const close = document.getElementById("exit"); close.onclick = () => { - ipcRenderer.send("main", "quit"); -} + ipcRenderer.send("main", "quit"); +}; diff --git a/e2e/window-test/constants.js b/e2e/window-test/constants.js index 8b7a520c..7cda18ea 100644 --- a/e2e/window-test/constants.js +++ b/e2e/window-test/constants.js @@ -5,9 +5,9 @@ const HEIGTH = 300; const TITLE = "libnut window test"; module.exports = { - POS_X, - POS_Y, - WIDTH, - HEIGTH, - TITLE -}; \ No newline at end of file + POS_X, + POS_Y, + WIDTH, + HEIGTH, + TITLE, +}; diff --git a/e2e/window-test/index.css b/e2e/window-test/index.css index 4b175247..118138dd 100644 --- a/e2e/window-test/index.css +++ b/e2e/window-test/index.css @@ -1,19 +1,19 @@ body { - width: 100vw; - height: 100vh; + width: 100vw; + height: 100vh; } #content { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 100vh; - width: 100vw; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; } #exit { - color: white; - font-size: 1.5rem; - background: darkblue; -} \ No newline at end of file + color: white; + font-size: 1.5rem; + background: darkblue; +} diff --git a/e2e/window-test/index.html b/e2e/window-test/index.html index 7aceec05..1700b83b 100644 --- a/e2e/window-test/index.html +++ b/e2e/window-test/index.html @@ -1,10 +1,13 @@ - + - - + + libnut window test diff --git a/e2e/window-test/main.js b/e2e/window-test/main.js index c9e83602..2ecbd6e4 100644 --- a/e2e/window-test/main.js +++ b/e2e/window-test/main.js @@ -1,37 +1,37 @@ -const {app, ipcMain, BrowserWindow} = require('electron') -const path = require('path'); +const { app, ipcMain, BrowserWindow } = require("electron"); +const path = require("path"); const { POS_X, POS_Y, WIDTH, HEIGTH } = require("./constants"); function createWindow() { - const mainWindow = new BrowserWindow({ - width: WIDTH, - height: HEIGTH, - alwaysOnTop: true, - webPreferences: { - nodeIntegration: true, - preload: path.join(__dirname, 'preload.js') - } - }); - mainWindow.loadFile(path.join(__dirname, "index.html")); - mainWindow.setPosition(POS_X, POS_Y); + const mainWindow = new BrowserWindow({ + width: WIDTH, + height: HEIGTH, + alwaysOnTop: true, + webPreferences: { + nodeIntegration: true, + preload: path.join(__dirname, "preload.js"), + }, + }); + mainWindow.loadFile(path.join(__dirname, "index.html")); + mainWindow.setPosition(POS_X, POS_Y); } ipcMain.on("main", (event, args) => { - if (args === "quit") { - app.quit(); - } + if (args === "quit") { + app.quit(); + } }); app.whenReady().then(() => { - setTimeout(() => app.exit(1), 15000); - createWindow() + setTimeout(() => app.exit(1), 15000); + createWindow(); - app.on('activate', function () { - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) -}) + app.on("activate", function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); -app.on('window-all-closed', function () { - console.log("Bye!"); - app.quit(); -}) +app.on("window-all-closed", function () { + console.log("Bye!"); + app.quit(); +}); diff --git a/e2e/window-test/renderer.js b/e2e/window-test/renderer.js index c3f88f49..be415018 100644 --- a/e2e/window-test/renderer.js +++ b/e2e/window-test/renderer.js @@ -1,6 +1,6 @@ -const {ipcRenderer} = require("electron"); +const { ipcRenderer } = require("electron"); -const close = document.getElementById('exit'); +const close = document.getElementById("exit"); close.onclick = () => { - ipcRenderer.send("main", "quit"); -} + ipcRenderer.send("main", "quit"); +}; diff --git a/e2e/window-test/test.js b/e2e/window-test/test.js index e76cc23c..3e71a52a 100644 --- a/e2e/window-test/test.js +++ b/e2e/window-test/test.js @@ -1,105 +1,105 @@ const Application = require("spectron").Application; const electronPath = require("electron"); -const {getActiveWindow, getWindows} = require("@nut-tree/nut-js"); -const {POS_X, POS_Y, WIDTH, HEIGTH, TITLE} = require("./constants"); -const {join} = require("path"); +const { getActiveWindow, getWindows } = require("@nut-tree/nut-js"); +const { POS_X, POS_Y, WIDTH, HEIGTH, TITLE } = require("./constants"); +const { join } = require("path"); const sleep = async (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); }; let app; const APP_TIMEOUT = 10000; -jest.setTimeout(3 * APP_TIMEOUT) +jest.setTimeout(3 * APP_TIMEOUT); beforeEach(async () => { - app = new Application({ - path: electronPath, - args: [join(__dirname, 'main.js')], - startTimeout: APP_TIMEOUT, - waitTimeout: APP_TIMEOUT, - }); - await app.start(); - await app.client.waitUntilWindowLoaded(); - await app.browserWindow.minimize(); - await app.browserWindow.restore(); - await app.browserWindow.focus(); + app = new Application({ + path: electronPath, + args: [join(__dirname, "main.js")], + startTimeout: APP_TIMEOUT, + waitTimeout: APP_TIMEOUT, + }); + await app.start(); + await app.client.waitUntilWindowLoaded(); + await app.browserWindow.minimize(); + await app.browserWindow.restore(); + await app.browserWindow.focus(); }); describe("getWindows", () => { - it("should list our started application window", async () => { - // GIVEN - const openWindows = await getWindows(); + it("should list our started application window", async () => { + // GIVEN + const openWindows = await getWindows(); - // WHEN - const windowNames = await Promise.all(openWindows.map((wnd) => wnd.title)); + // WHEN + const windowNames = await Promise.all(openWindows.map((wnd) => wnd.title)); - // THEN - expect(windowNames).toContain(TITLE); - }); + // THEN + expect(windowNames).toContain(TITLE); + }); }); describe("getActiveWindow", () => { - it("should return our started application window", async () => { - // GIVEN - - // WHEN - const foregroundWindow = await getActiveWindow(); - const windowTitle = await foregroundWindow.title; - - // THEN - expect(windowTitle).toBe(TITLE); - }); - - it("should determine correct coordinates for our application", async () => { - // GIVEN - - // WHEN - const foregroundWindow = await getActiveWindow(); - const activeWindowRegion = await foregroundWindow.region; - - // THEN - expect(activeWindowRegion.left).toBe(POS_X); - expect(activeWindowRegion.top).toBe(POS_Y); - expect(activeWindowRegion.width).toBe(WIDTH); - expect(activeWindowRegion.height).toBe(HEIGTH); - }); - - it("should determine correct coordinates for our application after moving the window", async () => { - // GIVEN - const xPosition = 42; - const yPosition = 25; - await app.browserWindow.setPosition(xPosition, yPosition); - await sleep(1000); - - // WHEN - const foregroundWindow = await getActiveWindow(); - const activeWindowRegion = await foregroundWindow.region; - - // THEN - expect(activeWindowRegion.left).toBe(xPosition); - expect(activeWindowRegion.top).toBe(yPosition); - }); - - it("should determine correct window size for our application after resizing the window", async () => { - // GIVEN - const newWidth = 400; - const newHeight = 350; - await app.browserWindow.setSize(newWidth, newHeight); - await sleep(1000); - - // WHEN - const foregroundWindow = await getActiveWindow(); - const activeWindowRegion = await foregroundWindow.region; - - // THEN - expect(activeWindowRegion.width).toBe(newWidth); - expect(activeWindowRegion.height).toBe(newHeight); - }); + it("should return our started application window", async () => { + // GIVEN + + // WHEN + const foregroundWindow = await getActiveWindow(); + const windowTitle = await foregroundWindow.title; + + // THEN + expect(windowTitle).toBe(TITLE); + }); + + it("should determine correct coordinates for our application", async () => { + // GIVEN + + // WHEN + const foregroundWindow = await getActiveWindow(); + const activeWindowRegion = await foregroundWindow.region; + + // THEN + expect(activeWindowRegion.left).toBe(POS_X); + expect(activeWindowRegion.top).toBe(POS_Y); + expect(activeWindowRegion.width).toBe(WIDTH); + expect(activeWindowRegion.height).toBe(HEIGTH); + }); + + it("should determine correct coordinates for our application after moving the window", async () => { + // GIVEN + const xPosition = 42; + const yPosition = 25; + await app.browserWindow.setPosition(xPosition, yPosition); + await sleep(1000); + + // WHEN + const foregroundWindow = await getActiveWindow(); + const activeWindowRegion = await foregroundWindow.region; + + // THEN + expect(activeWindowRegion.left).toBe(xPosition); + expect(activeWindowRegion.top).toBe(yPosition); + }); + + it("should determine correct window size for our application after resizing the window", async () => { + // GIVEN + const newWidth = 400; + const newHeight = 350; + await app.browserWindow.setSize(newWidth, newHeight); + await sleep(1000); + + // WHEN + const foregroundWindow = await getActiveWindow(); + const activeWindowRegion = await foregroundWindow.region; + + // THEN + expect(activeWindowRegion.width).toBe(newWidth); + expect(activeWindowRegion.height).toBe(newHeight); + }); }); afterEach(async () => { - if (app && app.isRunning()) { - await app.stop(); - } + if (app && app.isRunning()) { + await app.stop(); + } }); diff --git a/index.ts b/index.ts index e0336f23..34c5f059 100644 --- a/index.ts +++ b/index.ts @@ -1,41 +1,41 @@ -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 { 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"; +import { loadImageResource } from "./lib/imageResources.function"; export { - AssertClass, - ClipboardClass, - KeyboardClass, - MouseClass, - ScreenClass, - providerRegistry -} + AssertClass, + ClipboardClass, + KeyboardClass, + MouseClass, + ScreenClass, + providerRegistry, +}; -export {MatchRequest} from "./lib/match-request.class"; -export {MatchResult} from "./lib/match-result.class"; +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"; +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(); @@ -45,29 +45,37 @@ 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 { straightTo, up, down, left, right } = createMovementApi( + providerRegistry, + lineHelper +); +const { getWindows, getActiveWindow } = createWindowApi(providerRegistry); const loadImage = providerRegistry.getImageReader().load; const saveImage = providerRegistry.getImageWriter().store; -const imageResource = (fileName: string) => loadImageResource(providerRegistry, screen.config.resourceDirectory, fileName); -export {fetchFromUrl} from "./lib/imageResources.function"; +const imageResource = (fileName: string) => + loadImageResource( + providerRegistry, + screen.config.resourceDirectory, + fileName + ); +export { fetchFromUrl } from "./lib/imageResources.function"; export { - clipboard, - keyboard, - mouse, - screen, - assert, - straightTo, - up, - down, - left, - right, - getWindows, - getActiveWindow, - loadImage, - saveImage, - imageResource + clipboard, + keyboard, + mouse, + screen, + assert, + straightTo, + up, + down, + left, + right, + getWindows, + getActiveWindow, + loadImage, + saveImage, + imageResource, }; diff --git a/jest.config.js b/jest.config.js index 85056493..e870b4f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,11 +7,14 @@ module.exports = { ], preset: "ts-jest", testEnvironment: "node", - testMatch: process.env.E2E_TEST ? - ["**/__tests__/(e2e)/**/*.[jt]s?(x)", "**/?(*.)(e2e.)+(spec|test).[jt]s?(x)"] : - ["**/__tests__/!(e2e)/**/*.[jt]s?(x)", "**/!(*.e2e.*)+(spec|test).[jt]s?(x)"], - testPathIgnorePatterns: [ - "/node_modules/", - "/dist/", - ], + testMatch: process.env.E2E_TEST + ? [ + "**/__tests__/(e2e)/**/*.[jt]s?(x)", + "**/?(*.)(e2e.)+(spec|test).[jt]s?(x)", + ] + : [ + "**/__tests__/!(e2e)/**/*.[jt]s?(x)", + "**/!(*.e2e.*)+(spec|test).[jt]s?(x)", + ], + testPathIgnorePatterns: ["/node_modules/", "/dist/"], }; diff --git a/lib/assert.class.spec.ts b/lib/assert.class.spec.ts index 8f3b1e0b..5064b8db 100644 --- a/lib/assert.class.spec.ts +++ b/lib/assert.class.spec.ts @@ -1,93 +1,99 @@ -import {AssertClass} from "./assert.class"; -import {Region} from "./region.class"; -import {ScreenClass} from "./screen.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"; +import { Image } from "../index"; +import { mockPartial } from "sneer"; -jest.mock('jimp', () => { -}); +jest.mock("jimp", () => {}); jest.mock("./screen.class"); const needleId = "needleId"; describe("Assert", () => { - 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 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, }); - 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 - // WHEN + // THEN + await expect(SUT.isVisible(needle)).resolves.not.toThrowError(); + }); - // 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 needle = mockPartial({ + id: needleId, }); - 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()}` - ); + // 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, }); - 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`); + // 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, }); - 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)).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 + // WHEN - // THEN - await expect(SUT.notVisible(needle)).resolves.not.toThrowError(); - }); + // THEN + await expect(SUT.notVisible(needle)).resolves.not.toThrowError(); + }); }); diff --git a/lib/assert.class.ts b/lib/assert.class.ts index 6409e791..06b1dbe7 100644 --- a/lib/assert.class.ts +++ b/lib/assert.class.ts @@ -1,42 +1,49 @@ -import {Region} from "./region.class"; -import {ScreenClass} from "./screen.class"; -import {FirstArgumentType} from "./typings"; -import {OptionalSearchParameters} from "./optionalsearchparameters.class"; +import { Region } from "./region.class"; +import { ScreenClass } from "./screen.class"; +import { FirstArgumentType } from "./typings"; +import { OptionalSearchParameters } from "./optionalsearchparameters.class"; export class AssertClass { - constructor(private screen: ScreenClass) { - } + constructor(private screen: ScreenClass) {} - public async isVisible(needle: FirstArgumentType, searchRegion?: Region, confidence?: number) { - const identifier = (await needle).id; + public async isVisible( + needle: FirstArgumentType, + searchRegion?: Region, + confidence?: number + ) { + const identifier = (await needle).id; - 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}`); - } - } + 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(needle: FirstArgumentType, searchRegion?: Region, confidence?: number) { - const identifier = (await needle).id; + 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`); + try { + await this.screen.find(needle, { + searchRegion, + confidence, + } as OptionalSearchParameters); + } catch (err) { + return; } + throw new Error(`'${identifier}' is visible`); + } } diff --git a/lib/clipboard.class.e2e.spec.ts b/lib/clipboard.class.e2e.spec.ts index d4e35f1d..c1844258 100644 --- a/lib/clipboard.class.e2e.spec.ts +++ b/lib/clipboard.class.e2e.spec.ts @@ -1,13 +1,13 @@ -import {ClipboardClass} 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 SUT = new ClipboardClass(providerRegistry); + it("should paste copied input from system clipboard.", async () => { + const SUT = new ClipboardClass(providerRegistry); - const textToCopy = "bar"; + const textToCopy = "bar"; - SUT.copy(textToCopy); - await expect(SUT.paste()).resolves.toEqual(textToCopy); - }); + SUT.copy(textToCopy); + await expect(SUT.paste()).resolves.toEqual(textToCopy); + }); }); diff --git a/lib/clipboard.class.spec.ts b/lib/clipboard.class.spec.ts index c0119bc0..0cefb7f3 100644 --- a/lib/clipboard.class.spec.ts +++ b/lib/clipboard.class.spec.ts @@ -1,47 +1,50 @@ -import {ClipboardClass} from "./clipboard.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; -import {mockPartial} from "sneer"; -import {ClipboardProviderInterface} from "./provider"; +import { ClipboardClass } from "./clipboard.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; +import { mockPartial } from "sneer"; +import { ClipboardProviderInterface } from "./provider"; -jest.mock('jimp', () => { -}); +jest.mock("jimp", () => {}); beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); }); -const providerRegistryMock = mockPartial({}) +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); - - // THEN - expect(copyMock).toHaveBeenCalledTimes(1); - expect(copyMock).toHaveBeenCalledWith(textToCopy); - }); - - 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(); - - // THEN - expect(pasteMock).toHaveBeenCalledTimes(1); - }); + 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); + + // THEN + expect(copyMock).toHaveBeenCalledTimes(1); + expect(copyMock).toHaveBeenCalledWith(textToCopy); + }); + + 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(); + + // THEN + expect(pasteMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/lib/clipboard.class.ts b/lib/clipboard.class.ts index 67aa3744..1f08751c 100644 --- a/lib/clipboard.class.ts +++ b/lib/clipboard.class.ts @@ -1,15 +1,14 @@ /** * {@link ClipboardClass} class gives access to a systems clipboard */ -import {ProviderRegistry} from "./provider/provider-registry.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; export class ClipboardClass { /** * {@link ClipboardClass} class constructor * @param providerRegistry */ - constructor(private providerRegistry: ProviderRegistry) { - } + constructor(private providerRegistry: ProviderRegistry) {} /** * {@link copy} copies a given text to the system clipboard diff --git a/lib/colormode.enum.ts b/lib/colormode.enum.ts index ce0b0434..24b7cbcd 100644 --- a/lib/colormode.enum.ts +++ b/lib/colormode.enum.ts @@ -2,6 +2,6 @@ * 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 + BGR, + RGB, +} diff --git a/lib/expect/jest.matcher.function.ts b/lib/expect/jest.matcher.function.ts index 9477f27d..fb70fe3e 100644 --- a/lib/expect/jest.matcher.function.ts +++ b/lib/expect/jest.matcher.function.ts @@ -1,23 +1,26 @@ -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"; +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: FirstArgumentType, 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 c40ebb63..ed589c33 100644 --- a/lib/expect/matchers/toBeAt.function.e2e.spec.ts +++ b/lib/expect/matchers/toBeAt.function.e2e.spec.ts @@ -2,7 +2,7 @@ import { mouse } from "../../../index"; import { Point } from "../../point.class"; import { toBeAt } from "./toBeAt.function"; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); const targetPoint = new Point(100, 100); diff --git a/lib/expect/matchers/toBeIn.function.e2e.spec.ts b/lib/expect/matchers/toBeIn.function.e2e.spec.ts index ba3f2501..57269bc5 100644 --- a/lib/expect/matchers/toBeIn.function.e2e.spec.ts +++ b/lib/expect/matchers/toBeIn.function.e2e.spec.ts @@ -3,7 +3,7 @@ import { Point } from "../../point.class"; import { Region } from "../../region.class"; import { toBeIn } from "./toBeIn.function"; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); const targetPoint = new Point(400, 400); diff --git a/lib/expect/matchers/toShow.function.ts b/lib/expect/matchers/toShow.function.ts index 3b3c3848..c1542779 100644 --- a/lib/expect/matchers/toShow.function.ts +++ b/lib/expect/matchers/toShow.function.ts @@ -1,28 +1,28 @@ -import {ScreenClass} from "../../screen.class"; -import {FirstArgumentType} from "../../typings"; -import {OptionalSearchParameters} from "../../optionalsearchparameters.class"; +import { ScreenClass } from "../../screen.class"; +import { FirstArgumentType } from "../../typings"; +import { OptionalSearchParameters } from "../../optionalsearchparameters.class"; export const toShow = async ( - received: ScreenClass, - needle: FirstArgumentType, - confidence?: number, + received: ScreenClass, + needle: FirstArgumentType, + confidence?: number ) => { - 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, - }; - } + 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/file-type.enum.ts b/lib/file-type.enum.ts index 6980ba84..030db70f 100644 --- a/lib/file-type.enum.ts +++ b/lib/file-type.enum.ts @@ -3,5 +3,5 @@ */ export enum FileType { PNG = ".png", - JPG = ".jpg" + JPG = ".jpg", } diff --git a/lib/generate-output-path.function.spec.ts b/lib/generate-output-path.function.spec.ts index 2187b5e1..0b4a51ef 100644 --- a/lib/generate-output-path.function.spec.ts +++ b/lib/generate-output-path.function.spec.ts @@ -25,7 +25,7 @@ describe("generate-output-path", () => { const expectedPath = join(cwd(), `${pre}${filename}${ext}`); // WHEN - const result = generateOutputPath(filename, {prefix: pre}); + const result = generateOutputPath(filename, { prefix: pre }); // THEN expect(result).toEqual(expectedPath); @@ -39,7 +39,7 @@ describe("generate-output-path", () => { const expectedPath = join(cwd(), `${filename}${post}${ext}`); // WHEN - const result = generateOutputPath(filename, {postfix: post}); + const result = generateOutputPath(filename, { postfix: post }); // THEN expect(result).toEqual(expectedPath); @@ -103,7 +103,7 @@ describe("generate-output-path", () => { // WHEN const result = generateOutputPath(filename, { - type: FileType.JPG + type: FileType.JPG, }); // THEN diff --git a/lib/generate-output-path.function.ts b/lib/generate-output-path.function.ts index acc25d83..bd41eeed 100644 --- a/lib/generate-output-path.function.ts +++ b/lib/generate-output-path.function.ts @@ -10,16 +10,16 @@ import { FileType } from "./file-type.enum"; export const generateOutputPath = ( filename: string, params?: { - type?: FileType, - path?: string, - prefix?: string, - postfix?: string + type?: FileType; + path?: string; + prefix?: string; + postfix?: string; } ) => { const name = parse(filename).name; - const imageType = (params && params.type) ? params.type : FileType.PNG; - const path = (params && params.path) ? params.path : cwd(); - const prefix = (params && params.prefix) ? params.prefix : ""; - const postfix = (params && params.postfix) ? params.postfix : ""; + const imageType = params && params.type ? params.type : FileType.PNG; + const path = params && params.path ? params.path : cwd(); + const prefix = params && params.prefix ? params.prefix : ""; + const postfix = params && params.postfix ? params.postfix : ""; return join(path, `${prefix}${name}${postfix}${imageType}`); }; diff --git a/lib/image.class.spec.ts b/lib/image.class.spec.ts index 47d866dc..17fb3f16 100644 --- a/lib/image.class.spec.ts +++ b/lib/image.class.spec.ts @@ -1,129 +1,138 @@ -import {Image, isImage} from "./image.class"; -import {imageToJimp} from "./provider/io/imageToJimp.function"; -import {ColorMode} from "./colormode.enum"; +import { Image, isImage } 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() - } + return { + imageToJimp: jest.fn(), + }; }); afterEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks(); }); describe("Image class", () => { - 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 = 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 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 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); }); + }); - 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("isImage typeguard", () => { + it("should identify an Image", () => { + // GIVEN + const img = new Image(100, 100, Buffer.from([]), 4, "foo"); - 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 result = isImage(img); - // WHEN - const convertedImage = await bgrImage.toBGR(); + // THEN + expect(result).toBeTruthy(); + }); - // THEN - expect(convertedImage).toBe(bgrImage); - expect(imageToJimp).not.toBeCalledTimes(1) - }); + it("should rule out non-objects", () => { + // GIVEN + const i = "foo"; - 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 result = isImage(i); - // WHEN - const convertedImage = await rgbImage.toRGB(); + // THEN + expect(result).toBeFalsy(); + }); - // THEN - expect(convertedImage).toBe(rgbImage); - expect(imageToJimp).not.toBeCalledTimes(1) - }); + it("should rule out possible object with missing properties", () => { + // GIVEN + const img = { + width: 100, + height: 100, + data: Buffer.from([]), + channels: "foo", + id: "foo", + colorMode: ColorMode.BGR, + }; + + // WHEN + const result = isImage(img); + + // THEN + expect(result).toBeFalsy(); }); - describe('isImage typeguard', () => { - it('should identify an Image', () => { - // GIVEN - const img = new Image(100, 100, Buffer.from([]), 4, 'foo'); - - // WHEN - const result = isImage(img); - - // THEN - expect(result).toBeTruthy(); - }); - - it('should rule out non-objects', () => { - // GIVEN - const i = "foo"; - - // WHEN - const result = isImage(i); - - // THEN - expect(result).toBeFalsy(); - }); - - it('should rule out possible object with missing properties', () => { - // GIVEN - const img = { - width: 100, - height: 100, - data: Buffer.from([]), - channels: 'foo', - id: 'foo', - colorMode: ColorMode.BGR - }; - - // WHEN - const result = isImage(img); - - // THEN - expect(result).toBeFalsy(); - }); - - it('should rule out possible object with wrong property type', () => { - // GIVEN - const img = { - width: 100, - height: 100, - data: Buffer.from([]), - channels: 'foo', - id: 'foo', - colorMode: ColorMode.BGR, - pixelDensity: 25 - }; - - // WHEN - const result = isImage(img); - - // THEN - expect(result).toBeFalsy(); - }); - }) + it("should rule out possible object with wrong property type", () => { + // GIVEN + const img = { + width: 100, + height: 100, + data: Buffer.from([]), + channels: "foo", + id: "foo", + colorMode: ColorMode.BGR, + pixelDensity: 25, + }; + + // WHEN + const result = isImage(img); + + // THEN + expect(result).toBeFalsy(); + }); + }); }); diff --git a/lib/image.class.ts b/lib/image.class.ts index fb360225..ace79474 100644 --- a/lib/image.class.ts +++ b/lib/image.class.ts @@ -1,92 +1,114 @@ -import {imageToJimp} from "./provider/io/imageToJimp.function"; -import {ColorMode} from "./colormode.enum"; +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 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 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, } - - /** - * {@link hasAlphaChannel} return true if an {@link Image} has an additional (fourth) alpha channel - */ - public get hasAlphaChannel() { - return this.channels > 3; + ) { + if (channels <= 0) { + throw new Error("Channel <= 0"); } + } - /** - * {@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 hasAlphaChannel} return true if an {@link Image} has an additional (fourth) alpha channel + */ + public get hasAlphaChannel() { + return this.channels > 3; + } - /** - * {@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 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 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); + /** + * {@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 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); + } } const testImage = new Image(100, 100, Buffer.from([]), 4, "typeCheck"); const imageKeys = Object.keys(testImage); export function isImage(possibleImage: any): possibleImage is Image { - if (typeof possibleImage !== 'object') { - return false; + if (typeof possibleImage !== "object") { + return false; + } + for (const key of imageKeys) { + if (!(key in possibleImage)) { + return false; } - for (const key of imageKeys) { - if (!(key in possibleImage)) { - return false; - } - const possibleImageKeyType = typeof possibleImage[key]; - const imageKeyType = typeof testImage[key as keyof typeof testImage]; - if (possibleImageKeyType !== imageKeyType) { - return false - } + const possibleImageKeyType = typeof possibleImage[key]; + const imageKeyType = typeof testImage[key as keyof typeof testImage]; + if (possibleImageKeyType !== imageKeyType) { + return false; } - return true; -} \ No newline at end of file + } + return true; +} diff --git a/lib/imageResources.function.spec.ts b/lib/imageResources.function.spec.ts index 1a30f1ce..2c56f54c 100644 --- a/lib/imageResources.function.spec.ts +++ b/lib/imageResources.function.spec.ts @@ -1,71 +1,80 @@ -import {fetchFromUrl, loadImageResource} from "./imageResources.function"; -import {mockPartial} from "sneer"; -import {ProviderRegistry} from "./provider/provider-registry.class"; -import {ImageReader} from "./provider"; -import {join} from "path"; -import {ColorMode} from "./colormode.enum"; +import { fetchFromUrl, loadImageResource } from "./imageResources.function"; +import { mockPartial } from "sneer"; +import { ProviderRegistry } from "./provider/provider-registry.class"; +import { ImageReader } from "./provider"; +import { join } from "path"; +import { ColorMode } from "./colormode.enum"; const loadMock = jest.fn(); const providerRegistryMock = mockPartial({ - getImageReader(): ImageReader { - return mockPartial({ - load: loadMock - }); - } + 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"; +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); + // WHEN + await loadImageResource( + providerRegistryMock, + resourceDirectoryPath, + imageFileName + ); - // THEN - expect(loadMock).toBeCalledWith(join(resourceDirectoryPath, imageFileName)); - }); + // THEN + expect(loadMock).toBeCalledWith(join(resourceDirectoryPath, imageFileName)); + }); }); -describe('fetchFromUrl', () => { - it('should throw on malformed URLs', async () => { - // GIVEN - const malformedUrl = "foo"; +describe("fetchFromUrl", () => { + it("should throw on malformed URLs", async () => { + // GIVEN + const malformedUrl = "foo"; - // WHEN - const SUT = () => fetchFromUrl(malformedUrl); + // WHEN + const SUT = () => fetchFromUrl(malformedUrl); - // THEN - await expect(SUT).rejects.toThrowError("Failed to fetch image data. Reason: Invalid URL"); - }); + // THEN + await expect(SUT).rejects.toThrowError( + "Failed to fetch image data. Reason: Invalid URL" + ); + }); - it('should throw on non-image URLs', async () => { - // GIVEN - const nonImageUrl = 'https://www.npmjs.com/package/jimp'; + it("should throw on non-image URLs", async () => { + // GIVEN + const nonImageUrl = "https://www.npmjs.com/package/jimp"; - // WHEN - const SUT = () => fetchFromUrl(nonImageUrl); + // WHEN + const SUT = () => fetchFromUrl(nonImageUrl); - // THEN - await expect(SUT).rejects.toThrowError("Failed to parse image data. Reason: Could not find MIME for Buffer"); - }); + // THEN + await expect(SUT).rejects.toThrowError( + "Failed to parse image data. Reason: Could not find MIME for Buffer" + ); + }); - it('should return an RGB image from a valid URL', async () => { - // GIVEN - const validImageUrl = 'https://github.com/nut-tree/nut.js/raw/master/.gfx/nut.png'; - const expectedDimensions = { - width: 502, - height: 411 - }; - const expectedColorMode = ColorMode.RGB; + it("should return an RGB image from a valid URL", async () => { + // GIVEN + const validImageUrl = + "https://github.com/nut-tree/nut.js/raw/master/.gfx/nut.png"; + const expectedDimensions = { + width: 502, + height: 411, + }; + const expectedColorMode = ColorMode.RGB; - // WHEN - const rgbImage = await fetchFromUrl(validImageUrl); + // WHEN + const rgbImage = await fetchFromUrl(validImageUrl); - // THEN - expect(rgbImage.colorMode).toBe(expectedColorMode); - expect(rgbImage.width).toBe(expectedDimensions.width); - expect(rgbImage.height).toBe(expectedDimensions.height); - }); -}); \ No newline at end of file + // THEN + expect(rgbImage.colorMode).toBe(expectedColorMode); + expect(rgbImage.width).toBe(expectedDimensions.width); + expect(rgbImage.height).toBe(expectedDimensions.height); + }); +}); diff --git a/lib/imageResources.function.ts b/lib/imageResources.function.ts index 6d808355..14a0eb56 100644 --- a/lib/imageResources.function.ts +++ b/lib/imageResources.function.ts @@ -1,13 +1,17 @@ -import {join, normalize} from "path"; -import {ProviderRegistry} from "./provider/provider-registry.class"; -import {URL} from "url"; -import {Image} from "./image.class"; +import { join, normalize } from "path"; +import { ProviderRegistry } from "./provider/provider-registry.class"; +import { URL } from "url"; +import { Image } from "./image.class"; import Jimp from "jimp"; -import {ColorMode} from "./colormode.enum"; +import { ColorMode } from "./colormode.enum"; -export function loadImageResource(providerRegistry: ProviderRegistry, resourceDirectory: string, fileName: string) { - const fullPath = normalize(join(resourceDirectory, fileName)); - return providerRegistry.getImageReader().load(fullPath); +export function loadImageResource( + providerRegistry: ProviderRegistry, + resourceDirectory: string, + fileName: string +) { + const fullPath = normalize(join(resourceDirectory, fileName)); + return providerRegistry.getImageReader().load(fullPath); } /** @@ -16,28 +20,28 @@ export function loadImageResource(providerRegistry: ProviderRegistry, resourceDi * @throws On malformed URL input or in case of non-image remote content */ export async function fetchFromUrl(url: string | URL): Promise { - let imageUrl: URL; - if (url instanceof URL) { - imageUrl = url; - } else { - try { - imageUrl = new URL(url); - } catch (e: any) { - throw new Error(`Failed to fetch image data. Reason: ${e.message}`); - } + let imageUrl: URL; + if (url instanceof URL) { + imageUrl = url; + } else { + try { + imageUrl = new URL(url); + } catch (e: any) { + throw new Error(`Failed to fetch image data. Reason: ${e.message}`); } - return Jimp.read(imageUrl.href) - .then((image) => { - return new Image( - image.bitmap.width, - image.bitmap.height, - image.bitmap.data, - 4, - imageUrl.href, - ColorMode.RGB - ); - }) - .catch(err => { - throw new Error(`Failed to parse image data. Reason: ${err.message}`); - }); -} \ No newline at end of file + } + return Jimp.read(imageUrl.href) + .then((image) => { + return new Image( + image.bitmap.width, + image.bitmap.height, + image.bitmap.data, + 4, + imageUrl.href, + ColorMode.RGB + ); + }) + .catch((err) => { + throw new Error(`Failed to parse image data. Reason: ${err.message}`); + }); +} diff --git a/lib/key.enum.ts b/lib/key.enum.ts index f6f6ef0b..f708d9f3 100644 --- a/lib/key.enum.ts +++ b/lib/key.enum.ts @@ -140,5 +140,5 @@ export enum Key { AudioRewind, AudioForward, AudioRepeat, - AudioRandom + AudioRandom, } diff --git a/lib/keyboard.class.spec.ts b/lib/keyboard.class.spec.ts index 5c15158e..97019a24 100644 --- a/lib/keyboard.class.spec.ts +++ b/lib/keyboard.class.spec.ts @@ -1,196 +1,212 @@ -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"; +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.setTimeout(10000); beforeEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); }); const providerRegistryMock = mockPartial({ - getKeyboard(): KeyboardProviderInterface { - return mockPartial({ - setKeyboardDelay: jest.fn(), - }) - } -}) - -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); + getKeyboard(): KeyboardProviderInterface { + return mockPartial({ + setKeyboardDelay: jest.fn(), }); + }, +}); - 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); - } +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); - }); + // 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); + } - 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]; + // 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); + } - const keyMock = jest.fn(); - providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ - setKeyboardDelay: jest.fn(), - pressKey: keyMock - })); + // THEN + expect(keyMock).toHaveBeenCalledTimes(payload.length); + }); + + describe("autoDelayMs", () => { + it("pressKey should respect configured delay", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const delay = 100; + SUT.config.autoDelayMs = delay; + + const keyMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => + mockPartial({ + setKeyboardDelay: jest.fn(), + pressKey: keyMock, + }) + ); - // WHEN - for (const key of payload) { - await SUT.pressKey(key); - } + // WHEN + const start = Date.now(); + await SUT.pressKey(Key.A); + const duration = Date.now() - start; - // THEN - expect(keyMock).toHaveBeenCalledTimes(payload.length); + // THEN + expect(duration).toBeGreaterThanOrEqual(delay); }); 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); - }); + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const delay = 100; + SUT.config.autoDelayMs = delay; + + const keyMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => + mockPartial({ + setKeyboardDelay: jest.fn(), + releaseKey: keyMock, + }) + ); + + // WHEN + const start = Date.now(); + await SUT.releaseKey(Key.A); + const duration = Date.now() - start; - describe("autoDelayMs", () => { - it("pressKey should respect configured delay", async () => { - // GIVEN - const SUT = new KeyboardClass(providerRegistryMock); - const delay = 100; - SUT.config.autoDelayMs = delay; - - const keyMock = jest.fn(); - providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ - setKeyboardDelay: jest.fn(), - pressKey: keyMock - })); - - // WHEN - const start = Date.now(); - await SUT.pressKey(Key.A); - const duration = Date.now() - start; - - // THEN - expect(duration).toBeGreaterThanOrEqual(delay); - }); - - it("should pass a list of input keys down to the releaseKey call.", async () => { - // GIVEN - const SUT = new KeyboardClass(providerRegistryMock); - const delay = 100; - SUT.config.autoDelayMs = delay; - - const keyMock = jest.fn(); - providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ - setKeyboardDelay: jest.fn(), - releaseKey: keyMock - })); - - // WHEN - const start = Date.now(); - await SUT.releaseKey(Key.A); - const duration = Date.now() - start; - - // THEN - expect(duration).toBeGreaterThanOrEqual(delay); - }); + // THEN + expect(duration).toBeGreaterThanOrEqual(delay); }); + }); }); diff --git a/lib/keyboard.class.ts b/lib/keyboard.class.ts index 25dbf9ec..a0245812 100644 --- a/lib/keyboard.class.ts +++ b/lib/keyboard.class.ts @@ -1,107 +1,108 @@ -import {Key} from "./key.enum"; -import {sleep} from "./sleep.function"; -import {ProviderRegistry} from "./provider/provider-registry.class"; +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"); + return input.every((elem: string | Key) => typeof elem === "string"); }; /** * {@link KeyboardClass} class provides methods to emulate keyboard input */ export class KeyboardClass { - + /** + * Config object for {@link KeyboardClass} class + */ + public config = { /** - * Config object for {@link KeyboardClass} class + * Configures the delay between single key events */ - public config = { - /** - * Configures the delay between single key events - */ - autoDelayMs: 300, - }; + autoDelayMs: 300, + }; - /** - * {@link KeyboardClass} class constructor - * @param providerRegistry - */ - constructor(private providerRegistry: ProviderRegistry) { - this.providerRegistry.getKeyboard().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(" ")) { - 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 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(" ")) { + 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.V); - * ``` - * - * @param keys Array of {@link Key}s to press and hold - */ - public pressKey(...keys: Key[]): Promise { - return new Promise(async (resolve, reject) => { - try { - await sleep(this.config.autoDelayMs); - await this.providerRegistry.getKeyboard().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.V); + * ``` + * + * @param keys Array of {@link Key}s to press and hold + */ + public pressKey(...keys: Key[]): Promise { + return new Promise(async (resolve, reject) => { + try { + await sleep(this.config.autoDelayMs); + 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.V); - * ``` - * - * @param keys Array of {@link Key}s to release - */ - public releaseKey(...keys: Key[]): Promise { - return new Promise(async (resolve, reject) => { - try { - await sleep(this.config.autoDelayMs); - await this.providerRegistry.getKeyboard().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.V); + * ``` + * + * @param keys Array of {@link Key}s to release + */ + public releaseKey(...keys: Key[]): Promise { + return new Promise(async (resolve, reject) => { + try { + await sleep(this.config.autoDelayMs); + await this.providerRegistry.getKeyboard().releaseKey(...keys); + resolve(this); + } catch (e) { + reject(e); + } + }); + } } diff --git a/lib/location.function.spec.ts b/lib/location.function.spec.ts index c3f7ad10..0697b817 100644 --- a/lib/location.function.spec.ts +++ b/lib/location.function.spec.ts @@ -1,45 +1,49 @@ -import {centerOf, randomPointIn} from "./location.function"; -import {Point} from "./point.class"; -import {Region} from "./region.class"; +import { centerOf, randomPointIn } from "./location.function"; +import { Point } from "./point.class"; +import { Region } from "./region.class"; describe("Location", () => { - describe('centerOf', () => { - it("should return the center point of an area.", () => { - const expected = new Point(2, 2); - const testRegion = new Region(0, 0, 4, 4); + describe("centerOf", () => { + it("should return the center point of an area.", () => { + const expected = new Point(2, 2); + const testRegion = new Region(0, 0, 4, 4); - expect(centerOf(testRegion)).resolves.toEqual(expected); - }); + expect(centerOf(testRegion)).resolves.toEqual(expected); + }); - it("should throw on non Region input", async () => { - const testRegion = { - left: 0, - top: 0, - width: 4 - }; + it("should throw on non Region input", async () => { + const testRegion = { + left: 0, + top: 0, + width: 4, + }; - await expect(centerOf(testRegion as Region)).rejects.toThrowError(/^centerOf requires a Region, but received/); - }); + await expect(centerOf(testRegion as Region)).rejects.toThrowError( + /^centerOf requires a Region, but received/ + ); }); + }); - describe('randomPointIn', () => { - it("should return a random point inside of an area.", async () => { - const testRegion = new Region(100, 20, 50, 35); - const result = await randomPointIn(testRegion); - expect(result.x).toBeGreaterThanOrEqual(testRegion.left); - expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width); - expect(result.y).toBeGreaterThanOrEqual(testRegion.top); - expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height); - }); + describe("randomPointIn", () => { + it("should return a random point inside of an area.", async () => { + const testRegion = new Region(100, 20, 50, 35); + const result = await randomPointIn(testRegion); + expect(result.x).toBeGreaterThanOrEqual(testRegion.left); + expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width); + expect(result.y).toBeGreaterThanOrEqual(testRegion.top); + expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height); + }); - it("should throw on non Region input", async () => { - const testRegion = { - left: 0, - top: 0, - width: 4 - }; + it("should throw on non Region input", async () => { + const testRegion = { + left: 0, + top: 0, + width: 4, + }; - await expect(randomPointIn(testRegion as Region)).rejects.toThrowError(/^randomPointIn requires a Region, but received/); - }); + await expect(randomPointIn(testRegion as Region)).rejects.toThrowError( + /^randomPointIn requires a Region, but received/ + ); }); + }); }); diff --git a/lib/location.function.ts b/lib/location.function.ts index 56e14bd6..623f0a80 100644 --- a/lib/location.function.ts +++ b/lib/location.function.ts @@ -1,32 +1,42 @@ -import {Point} from "./point.class"; -import {isRegion, Region} from "./region.class"; +import { Point } from "./point.class"; +import { isRegion, Region } from "./region.class"; /** * {@link centerOf} returns the center {@link Point} for a given {@link Region} * @param target {@link Region} to determine the center {@link Point} for */ -export const centerOf = async (target: Region | Promise): Promise => { - const targetRegion = await target; - if (!isRegion(targetRegion)) { - throw Error(`centerOf requires a Region, but received ${JSON.stringify(targetRegion)}`) - } - const x = Math.floor(targetRegion.left + targetRegion.width / 2); - const y = Math.floor(targetRegion.top + targetRegion.height / 2); +export const centerOf = async ( + target: Region | Promise +): Promise => { + const targetRegion = await target; + if (!isRegion(targetRegion)) { + throw Error( + `centerOf requires a Region, but received ${JSON.stringify(targetRegion)}` + ); + } + const x = Math.floor(targetRegion.left + targetRegion.width / 2); + const y = Math.floor(targetRegion.top + targetRegion.height / 2); - return new Point(x, y); + return new Point(x, y); }; /** * {@link randomPointIn} returns a random {@link Point} within a given {@link Region} * @param target {@link Region} the random {@link Point} has to be within */ -export const randomPointIn = async (target: Region | Promise): Promise => { - const targetRegion = await target; - if (!isRegion(targetRegion)) { - throw Error(`randomPointIn requires a Region, but received ${JSON.stringify(targetRegion)}`) - } - const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width); - const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height); +export const randomPointIn = async ( + target: Region | Promise +): Promise => { + const targetRegion = await target; + if (!isRegion(targetRegion)) { + throw Error( + `randomPointIn requires a Region, but received ${JSON.stringify( + targetRegion + )}` + ); + } + const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width); + const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height); - return new Point(x, y); + return new Point(x, y); }; diff --git a/lib/match-request.class.spec.ts b/lib/match-request.class.spec.ts index d2eaaa76..2e029aec 100644 --- a/lib/match-request.class.spec.ts +++ b/lib/match-request.class.spec.ts @@ -1,27 +1,16 @@ -import {Image} from "./image.class"; -import {MatchRequest} from "./match-request.class"; +import { Image } from "./image.class"; +import { MatchRequest } from "./match-request.class"; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); describe("MatchRequest", () => { - 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); + 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 d09d026a..1b2d4e41 100644 --- a/lib/match-request.class.ts +++ b/lib/match-request.class.ts @@ -5,6 +5,6 @@ export class MatchRequest { public readonly haystack: Image, public readonly needle: Image, public readonly confidence: number, - public readonly searchMultipleScales: boolean = true, + public readonly searchMultipleScales: boolean = true ) {} } diff --git a/lib/mouse-movement.function.spec.ts b/lib/mouse-movement.function.spec.ts index ea176e21..9e9fbabe 100644 --- a/lib/mouse-movement.function.spec.ts +++ b/lib/mouse-movement.function.spec.ts @@ -1,80 +1,85 @@ import { - calculateStepDuration, - linear, - calculateMovementTimesteps, EasingFunction + 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; + 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); + // WHEN + const result = calculateStepDuration(speedInPixelsPerSecond); - // THEN - expect(result).toBe(expectedBaseStepDuration); - }); + // 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); + 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); + // WHEN + calculateMovementTimesteps( + amountOfSteps, + speedInPixelsPerSecond, + easingFunction + ); - // THEN - expect(easingFunction).toBeCalledTimes(amountOfSteps); - }) + // 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]; + 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); + // WHEN + const result = calculateMovementTimesteps(6, 1000, linear); - // THEN - expect(result).toEqual(expected); - }); + // 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]; + 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); + // WHEN + const result = calculateMovementTimesteps(6, 500, linear); - // THEN - expect(result).toEqual(expected); - }); + // 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; - } - return 0; - }; - const expected = [2000000, 2000000, 2000000, 1000000, 1000000, 1000000]; + 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; + } + return 0; + }; + const expected = [2000000, 2000000, 2000000, 1000000, 1000000, 1000000]; - // WHEN - const result = calculateMovementTimesteps(6, mouseSpeed, easingFunction); + // WHEN + const result = calculateMovementTimesteps(6, mouseSpeed, easingFunction); - // THEN - expect(result).toEqual(expected); - }); + // 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 index f67edd7a..95ccb4e3 100644 --- a/lib/mouse-movement.function.ts +++ b/lib/mouse-movement.function.ts @@ -4,31 +4,32 @@ * See https://easings.net/ for reference */ export interface EasingFunction { - (progressPercentage: number): number; + (progressPercentage: number): number; } const nanoSecondsPerSecond = 1_000_000_000; -export const calculateStepDuration = (speedInPixelsPerSecond: number) => (1 / speedInPixelsPerSecond) * nanoSecondsPerSecond; +export const calculateStepDuration = (speedInPixelsPerSecond: number) => + (1 / speedInPixelsPerSecond) * nanoSecondsPerSecond; export const calculateMovementTimesteps = ( - amountOfSteps: number, - speedInPixelsPerSecond: number, - easingFunction: EasingFunction = linear + amountOfSteps: number, + speedInPixelsPerSecond: number, + easingFunction: EasingFunction = linear ): number[] => { - const isEasingFunction = typeof easingFunction === "function"; - return Array(amountOfSteps) - .fill(speedInPixelsPerSecond) - .map((speed: number, idx: number) => { - let speedInPixels = speed; - if (isEasingFunction) { - speedInPixels += easingFunction(idx / amountOfSteps) * speedInPixels; - } - const stepDuration = calculateStepDuration(speedInPixels); - return (isFinite(stepDuration) && stepDuration > 0) ? stepDuration : 0; - }); + const isEasingFunction = typeof easingFunction === "function"; + return Array(amountOfSteps) + .fill(speedInPixelsPerSecond) + .map((speed: number, idx: number) => { + let speedInPixels = speed; + if (isEasingFunction) { + speedInPixels += easingFunction(idx / amountOfSteps) * speedInPixels; + } + const stepDuration = calculateStepDuration(speedInPixels); + return isFinite(stepDuration) && stepDuration > 0 ? stepDuration : 0; + }); }; export const linear: EasingFunction = (_: number): number => { - return 0; + return 0; }; diff --git a/lib/mouse.class.spec.ts b/lib/mouse.class.spec.ts index 35ff0f35..ae39b166 100644 --- a/lib/mouse.class.spec.ts +++ b/lib/mouse.class.spec.ts @@ -1,315 +1,350 @@ -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"; +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.clearAllMocks(); + jest.clearAllMocks(); }); const linehelper = new LineHelper(); const providerRegistryMock = mockPartial({ - getMouse(): MouseProviderInterface { - return mockPartial({ - setMouseDelay: jest.fn() - }) - } + getMouse(): MouseProviderInterface { + return mockPartial({ + setMouseDelay: jest.fn(), + }); + }, }); describe("Mouse class", () => { - 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 () => { + 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("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) => { // GIVEN const SUT = new MouseClass(providerRegistryMock); - const scrollAmount = 5; - - const scrollMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + const pressButtonMock = jest.fn(); + const releaseButtonMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ setMouseDelay: jest.fn(), - scrollLeft: scrollMock - })); + pressButton: pressButtonMock, + releaseButton: releaseButtonMock, + }) + ); // WHEN - const result = await SUT.scrollLeft(scrollAmount); + const pressed = await SUT.pressButton(input); + const released = await SUT.releaseButton(input); // THEN - expect(scrollMock).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); - - it("should forward scrollRight to the provider", async () => { + expect(pressButtonMock).toBeCalledWith(expected); + expect(releaseButtonMock).toBeCalledWith(expected); + expect(pressed).toBe(SUT); + expect(released).toBe(SUT); + } + ); + + describe("autoDelayMs", () => { + it("pressButton should respect configured delay", async () => { // GIVEN const SUT = new MouseClass(providerRegistryMock); - const scrollAmount = 5; + const delay = 100; + SUT.config.autoDelayMs = delay; - const scrollMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + const mouseMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ setMouseDelay: jest.fn(), - scrollRight: scrollMock - })); + pressButton: mouseMock, + }) + ); // WHEN - const result = await SUT.scrollRight(scrollAmount); + const start = Date.now(); + await SUT.pressButton(Button.LEFT); + const duration = Date.now() - start; // THEN - expect(scrollMock).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); + expect(duration).toBeGreaterThanOrEqual(delay); + }); - it("should forward scrollDown to the provider", async () => { + it("releaseButton should respect configured delay", async () => { // GIVEN const SUT = new MouseClass(providerRegistryMock); - const scrollAmount = 5; + const delay = 100; + SUT.config.autoDelayMs = delay; - const scrollMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + const mouseMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ setMouseDelay: jest.fn(), - scrollDown: scrollMock - })); + releaseButton: mouseMock, + }) + ); // WHEN - const result = await SUT.scrollDown(scrollAmount); + const start = Date.now(); + await SUT.releaseButton(Button.LEFT); + const duration = Date.now() - start; // THEN - expect(scrollMock).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); + expect(duration).toBeGreaterThanOrEqual(delay); + }); + }); + }); + + describe("click and doubleClick", () => { + describe("click", () => { + it.each([ + [Button.LEFT, Button.LEFT], + [Button.MIDDLE, Button.MIDDLE], + [Button.RIGHT, Button.RIGHT], + ] as Array<[Button, Button]>)( + "should click the respective button on the provider", + async (input: Button, expected: Button) => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ + setMouseDelay: jest.fn(), + click: clickMock, + }) + ); + + // WHEN + await SUT.click(input); + + // THEN + expect(clickMock).toBeCalledWith(expected); + } + ); }); - 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); + describe("doubleClick", () => { + it.each([ + [Button.LEFT, Button.LEFT], + [Button.MIDDLE, Button.MIDDLE], + [Button.RIGHT, Button.RIGHT], + ] as Array<[Button, Button]>)( + "should click the respective button on the provider", + async (input: Button, expected: Button) => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ + setMouseDelay: jest.fn(), + doubleClick: clickMock, + }) + ); + + // WHEN + await SUT.doubleClick(input); + + // THEN + expect(clickMock).toBeCalledWith(expected); + } + ); }); - it("update mouse position along path on move", async () => { + describe("leftClick", () => { + it("should use click internally", 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({ + const clickSpy = jest.spyOn(SUT, "click"); + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ setMouseDelay: jest.fn(), - setMousePosition: setPositionMock - })); + click: clickMock, + }) + ); // WHEN - const result = await SUT.move(path); + const result = await SUT.leftClick(); // THEN - expect(setPositionMock).toBeCalledTimes(path.length); + expect(clickSpy).toBeCalledWith(Button.LEFT); + expect(clickMock).toBeCalledWith(Button.LEFT); expect(result).toBe(SUT); + }); }); - it("should press and hold left mouse button, move and release left mouse button on drag", async () => { + describe("rightClick", () => { + it("should use click internally", 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({ + const clickSpy = jest.spyOn(SUT, "click"); + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => + mockPartial({ setMouseDelay: jest.fn(), - setMousePosition: setPositionMock, - pressButton: pressButtonMock, - releaseButton: releaseButtonMock - })); + click: clickMock, + }) + ); // WHEN - const result = await SUT.drag(path); + const result = await SUT.rightClick(); // THEN - expect(pressButtonMock).toBeCalledWith(Button.LEFT); - expect(setPositionMock).toBeCalledTimes(path.length); - expect(releaseButtonMock).toBeCalledWith(Button.LEFT); + expect(clickSpy).toBeCalledWith(Button.RIGHT); + expect(clickMock).toBeCalledWith(Button.RIGHT); 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) => { - // 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); - }); - - describe("autoDelayMs", () => { - it("pressButton should respect configured delay", async () => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - const delay = 100; - SUT.config.autoDelayMs = delay; - - const mouseMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - pressButton: mouseMock - })); - - // WHEN - const start = Date.now(); - await SUT.pressButton(Button.LEFT); - const duration = Date.now() - start; - - // THEN - expect(duration).toBeGreaterThanOrEqual(delay); - }); - - it("releaseButton should respect configured delay", async () => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - const delay = 100; - SUT.config.autoDelayMs = delay; - - const mouseMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - releaseButton: mouseMock - })); - - // WHEN - const start = Date.now(); - await SUT.releaseButton(Button.LEFT); - const duration = Date.now() - start; - - // THEN - expect(duration).toBeGreaterThanOrEqual(delay); - }); - }); - }); - - describe("click and doubleClick", () => { - describe("click", () => { - it.each([ - [Button.LEFT, Button.LEFT], - [Button.MIDDLE, Button.MIDDLE], - [Button.RIGHT, Button.RIGHT], - ] as Array<[Button, Button]>)("should click the respective button on the provider", async (input: Button, expected: Button) => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - const clickMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - click: clickMock, - })); - - // WHEN - await SUT.click(input); - - // THEN - expect(clickMock).toBeCalledWith(expected); - }); - }); - - describe("doubleClick", () => { - it.each([ - [Button.LEFT, Button.LEFT], - [Button.MIDDLE, Button.MIDDLE], - [Button.RIGHT, Button.RIGHT], - ] as Array<[Button, Button]>)("should click the respective button on the provider", async (input: Button, expected: Button) => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - const clickMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - doubleClick: clickMock, - })); - - // WHEN - await SUT.doubleClick(input); - - // THEN - expect(clickMock).toBeCalledWith(expected); - }); - }); - - describe("leftClick", () => { - it("should use click internally", async () => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - - const clickSpy = jest.spyOn(SUT, "click"); - const clickMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - click: clickMock - })); - - // WHEN - const result = await SUT.leftClick(); - - // THEN - expect(clickSpy).toBeCalledWith(Button.LEFT); - expect(clickMock).toBeCalledWith(Button.LEFT); - expect(result).toBe(SUT); - }); - }); - - describe("rightClick", () => { - it("should use click internally", async () => { - // GIVEN - const SUT = new MouseClass(providerRegistryMock); - - const clickSpy = jest.spyOn(SUT, "click"); - const clickMock = jest.fn(); - providerRegistryMock.getMouse = jest.fn(() => mockPartial({ - setMouseDelay: jest.fn(), - click: clickMock - })); - - // WHEN - const result = await SUT.rightClick(); - - // THEN - expect(clickSpy).toBeCalledWith(Button.RIGHT); - expect(clickMock).toBeCalledWith(Button.RIGHT); - expect(result).toBe(SUT); - }); - }); - }); + }); }); diff --git a/lib/mouse.class.ts b/lib/mouse.class.ts index 98a4cbde..9091e776 100644 --- a/lib/mouse.class.ts +++ b/lib/mouse.class.ts @@ -1,246 +1,259 @@ -import {Button} from "./button.enum"; -import {isPoint, 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"; +import { Button } from "./button.enum"; +import { isPoint, 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 MouseClass} class provides methods to emulate mouse input */ export class MouseClass { + /** + * Config object for {@link MouseClass} class + */ + public config = { /** - * Config object for {@link MouseClass} class + * Configures the delay between single mouse events */ - public config = { - /** - * Configures the delay between single mouse events - */ - autoDelayMs: 100, - - /** - * Configures the speed in pixels/second for mouse movement - */ - mouseSpeed: 1000, - }; + autoDelayMs: 100, /** - * {@link MouseClass} class constructor - * @param providerRegistry + * Configures the speed in pixels/second for mouse movement */ - constructor(private providerRegistry: ProviderRegistry) { - this.providerRegistry.getMouse().setMouseDelay(0); - } + mouseSpeed: 1000, + }; - /** - * {@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 { - if (!isPoint(target)) { - throw Error(`setPosition requires a Point, but received ${JSON.stringify(target)}`) - } - return new Promise(async (resolve, reject) => { - try { - await this.providerRegistry.getMouse().setMousePosition(target); - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link MouseClass} class constructor + * @param providerRegistry + */ + constructor(private providerRegistry: ProviderRegistry) { + this.providerRegistry.getMouse().setMouseDelay(0); + } - /** - * {@link getPosition} returns a {@link Point} representing the current mouse position - */ - public getPosition(): Promise { - return this.providerRegistry.getMouse().currentMousePosition(); + /** + * {@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 { + if (!isPoint(target)) { + throw Error( + `setPosition requires a Point, but received ${JSON.stringify(target)}` + ); } + return new Promise(async (resolve, reject) => { + try { + await this.providerRegistry.getMouse().setMousePosition(target); + 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.setPosition(node); - } - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link getPosition} returns a {@link Point} representing the current mouse position + */ + public getPosition(): Promise { + return this.providerRegistry.getMouse().currentMousePosition(); + } - /** - * {@link leftClick} performs a click with the left mouse button - */ - public async leftClick(): Promise { - return this.click(Button.LEFT); - } + /** + * {@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.setPosition(node); + } + resolve(this); + } catch (e) { + reject(e); + } + }); + } - /** - * {@link rightClick} performs a click with the right mouse button - */ - public async rightClick(): Promise { - return this.click(Button.RIGHT); - } + /** + * {@link leftClick} performs a click with the left mouse button + */ + public async leftClick(): Promise { + return this.click(Button.LEFT); + } - /** - * {@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 rightClick} performs a click with the right mouse button + */ + public async rightClick(): Promise { + return this.click(Button.RIGHT); + } - /** - * {@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 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 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 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 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 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 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 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 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 sleep(this.config.autoDelayMs); - await this.providerRegistry.getMouse().pressButton(btn); - 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 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 sleep(this.config.autoDelayMs); - await this.providerRegistry.getMouse().releaseButton(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 sleep(this.config.autoDelayMs); + await this.providerRegistry.getMouse().pressButton(btn); + resolve(this); + } catch (e) { + reject(e); + } + }); + } - /** - * {@link click} clicks a mouse button - * @param btn The {@link Button} to click - */ - public async click(btn: Button): Promise { - return new Promise(async (resolve, reject) => { - try { - await sleep(this.config.autoDelayMs); - await this.providerRegistry.getMouse().click(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 sleep(this.config.autoDelayMs); + await this.providerRegistry.getMouse().releaseButton(btn); + resolve(this); + } catch (e) { + reject(e); + } + }); + } - /** - * {@link doubleClick} performs a double click on a mouse button - * @param btn The {@link Button} to click - */ - public async doubleClick(btn: Button): Promise { - return new Promise(async (resolve, reject) => { - try { - await sleep(this.config.autoDelayMs); - await this.providerRegistry.getMouse().doubleClick(btn); - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link click} clicks a mouse button + * @param btn The {@link Button} to click + */ + public async click(btn: Button): Promise { + return new Promise(async (resolve, reject) => { + try { + await sleep(this.config.autoDelayMs); + await this.providerRegistry.getMouse().click(btn); + resolve(this); + } catch (e) { + reject(e); + } + }); + } + + /** + * {@link doubleClick} performs a double click on a mouse button + * @param btn The {@link Button} to click + */ + public async doubleClick(btn: Button): Promise { + return new Promise(async (resolve, reject) => { + try { + await sleep(this.config.autoDelayMs); + await this.providerRegistry.getMouse().doubleClick(btn); + resolve(this); + } catch (e) { + reject(e); + } + }); + } } diff --git a/lib/movement.function.ts b/lib/movement.function.ts index 41708d43..f5ae6652 100644 --- a/lib/movement.function.ts +++ b/lib/movement.function.ts @@ -1,33 +1,40 @@ -import {MovementApi} from "./movement-api.interface"; -import {isPoint, Point} from "./point.class"; -import {LineHelper} from "./util/linehelper.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; +import { MovementApi } from "./movement-api.interface"; +import { isPoint, Point } from "./point.class"; +import { LineHelper } from "./util/linehelper.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; -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; - if (!isPoint(targetPoint)) { - throw Error(`straightTo requires a Point, but received ${JSON.stringify(targetPoint)}`) - } - 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)); - }, - }); +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; + if (!isPoint(targetPoint)) { + throw Error( + `straightTo requires a Point, but received ${JSON.stringify( + targetPoint + )}` + ); + } + 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/optionalsearchparameters.class.ts b/lib/optionalsearchparameters.class.ts index 3900df4e..babc01d1 100644 --- a/lib/optionalsearchparameters.class.ts +++ b/lib/optionalsearchparameters.class.ts @@ -1,17 +1,21 @@ -import {Region} from "./region.class"; -import {AbortSignal} from "node-abort-controller"; +import { Region } from "./region.class"; +import { AbortSignal } from "node-abort-controller"; /** * {@link OptionalSearchParameters} serves as a data class holding location parameters for image search */ export class OptionalSearchParameters { - /** - * {@link OptionalSearchParameters} class constructor - * @param searchRegion Optional {@link Region} to limit the search space to - * @param confidence Optional confidence value to configure image match confidence - * @param searchMultipleScales Optional flag to indicate if the search should be conducted at different scales - * @param abort An {@link AbortSignal} to cancel an ongoing call to `waitFor` - */ - constructor(public searchRegion?: Region, public confidence?: number, public searchMultipleScales?: boolean, public abort?: AbortSignal) { - } + /** + * {@link OptionalSearchParameters} class constructor + * @param searchRegion Optional {@link Region} to limit the search space to + * @param confidence Optional confidence value to configure image match confidence + * @param searchMultipleScales Optional flag to indicate if the search should be conducted at different scales + * @param abort An {@link AbortSignal} to cancel an ongoing call to `waitFor` + */ + constructor( + public searchRegion?: Region, + public confidence?: number, + public searchMultipleScales?: boolean, + public abort?: AbortSignal + ) {} } diff --git a/lib/point.class.spec.ts b/lib/point.class.spec.ts index e1024bbd..ebb69699 100644 --- a/lib/point.class.spec.ts +++ b/lib/point.class.spec.ts @@ -1,61 +1,61 @@ -import {isPoint, Point} from "./point.class"; +import { isPoint, Point } from "./point.class"; describe("Point", () => { - it("should return a proper string representation.", () => { - const point = new Point(10, 15); - const expected = "(10, 15)"; + it("should return a proper string representation.", () => { + const point = new Point(10, 15); + const expected = "(10, 15)"; - expect(point.toString()).toEqual(expected); - }); + expect(point.toString()).toEqual(expected); + }); - describe('isPoint typeguard', () => { - it('should identify a Point', () => { - // GIVEN - const p = new Point(100, 100); + describe("isPoint typeguard", () => { + it("should identify a Point", () => { + // GIVEN + const p = new Point(100, 100); - // WHEN - const result = isPoint(p); + // WHEN + const result = isPoint(p); - // THEN - expect(result).toBeTruthy(); - }); + // THEN + expect(result).toBeTruthy(); + }); - it('should rule out non-objects', () => { - // GIVEN - const p = "foo"; + it("should rule out non-objects", () => { + // GIVEN + const p = "foo"; - // WHEN - const result = isPoint(p); + // WHEN + const result = isPoint(p); - // THEN - expect(result).toBeFalsy(); - }); + // THEN + expect(result).toBeFalsy(); + }); - it('should rule out possible object with missing properties', () => { - // GIVEN - const p = { - x: 100 - }; + it("should rule out possible object with missing properties", () => { + // GIVEN + const p = { + x: 100, + }; - // WHEN - const result = isPoint(p); + // WHEN + const result = isPoint(p); - // THEN - expect(result).toBeFalsy(); - }); + // THEN + expect(result).toBeFalsy(); + }); - it('should rule out possible object with wrong property type', () => { - // GIVEN - const p = { - x: 100, - y: 'foo' - }; + it("should rule out possible object with wrong property type", () => { + // GIVEN + const p = { + x: 100, + y: "foo", + }; - // WHEN - const result = isPoint(p); + // WHEN + const result = isPoint(p); - // THEN - expect(result).toBeFalsy(); - }); - }) + // THEN + expect(result).toBeFalsy(); + }); + }); }); diff --git a/lib/point.class.ts b/lib/point.class.ts index 3c536a95..6b45b6b8 100644 --- a/lib/point.class.ts +++ b/lib/point.class.ts @@ -10,7 +10,7 @@ const testPoint = new Point(100, 100); const pointKeys = Object.keys(testPoint); export function isPoint(possiblePoint: any): possiblePoint is Point { - if (typeof possiblePoint !== 'object') { + if (typeof possiblePoint !== "object") { return false; } for (const key of pointKeys) { @@ -19,8 +19,8 @@ export function isPoint(possiblePoint: any): possiblePoint is Point { } const possiblePointKeyType = typeof possiblePoint[key]; const pointKeyType = typeof testPoint[key as keyof typeof testPoint]; - if (possiblePointKeyType!== pointKeyType) { - return false + if (possiblePointKeyType !== pointKeyType) { + return false; } } return true; diff --git a/lib/provider/image-processor.interface.ts b/lib/provider/image-processor.interface.ts index 1eca6ffb..c77be27b 100644 --- a/lib/provider/image-processor.interface.ts +++ b/lib/provider/image-processor.interface.ts @@ -1,6 +1,6 @@ -import {Point} from "../point.class"; -import {RGBA} from "../rgba.class"; -import {Image} from "../image.class"; +import { Point } from "../point.class"; +import { RGBA } from "../rgba.class"; +import { Image } from "../image.class"; /** * An ImageProcessor should provide an abstraction layer to perform @@ -9,11 +9,13 @@ import {Image} from "../image.class"; * @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 + /** + * {@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; +} diff --git a/lib/provider/image-reader.type.ts b/lib/provider/image-reader.type.ts index 04b8c4f1..6733fc22 100644 --- a/lib/provider/image-reader.type.ts +++ b/lib/provider/image-reader.type.ts @@ -1,4 +1,4 @@ -import {DataSourceInterface} from "./data-source.interface"; -import {Image} from "../image.class"; +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 index 0d063fee..d8eceaf6 100644 --- a/lib/provider/image-writer.type.ts +++ b/lib/provider/image-writer.type.ts @@ -1,9 +1,9 @@ -import {Image} from "../image.class"; -import {DataSinkInterface} from "./data-sink.interface"; +import { Image } from "../image.class"; +import { DataSinkInterface } from "./data-sink.interface"; export interface ImageWriterParameters { - image: Image, - path: string + 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 index 832f71fe..4ba6d335 100644 --- a/lib/provider/image/jimp-image-processor.class.ts +++ b/lib/provider/image/jimp-image-processor.class.ts @@ -1,24 +1,33 @@ -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"; +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)); - }); - } + 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 index f20d3799..aeb2ea2c 100644 --- a/lib/provider/index.ts +++ b/lib/provider/index.ts @@ -1,10 +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 +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"; diff --git a/lib/provider/io/imageToJimp.function.spec.ts b/lib/provider/io/imageToJimp.function.spec.ts index 30c527c1..74ded2c4 100644 --- a/lib/provider/io/imageToJimp.function.spec.ts +++ b/lib/provider/io/imageToJimp.function.spec.ts @@ -1,38 +1,44 @@ -import {Image} from "../../image.class"; -import {imageToJimp} from "./imageToJimp.function"; +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())) - } +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 - }) + 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"); +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); + // WHEN + const result = await imageToJimp(inputImage); - // THEN - expect(result).toBeInstanceOf(Jimp); - expect(scanMock).toHaveBeenCalledTimes(1); - }); -}); \ No newline at end of file + // THEN + expect(result).toBeInstanceOf(Jimp); + expect(scanMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/lib/provider/io/imageToJimp.function.ts b/lib/provider/io/imageToJimp.function.ts index b91cdff4..a1253290 100644 --- a/lib/provider/io/imageToJimp.function.ts +++ b/lib/provider/io/imageToJimp.function.ts @@ -1,20 +1,26 @@ import Jimp from "jimp"; -import {Image} from "../../image.class"; -import {ColorMode} from "../../colormode.enum"; +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 + 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; +} diff --git a/lib/provider/io/jimp-image-reader.class.spec.ts b/lib/provider/io/jimp-image-reader.class.spec.ts index 837b6d5c..bd1267e7 100644 --- a/lib/provider/io/jimp-image-reader.class.spec.ts +++ b/lib/provider/io/jimp-image-reader.class.spec.ts @@ -1,58 +1,58 @@ import ImageReader from "./jimp-image-reader.class"; -import {join} from "path"; +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 - }) +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); +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); }); - 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 + // WHEN + try { + await SUT.load(inputPath); + } catch (err) { + // THEN + expect(err).toStrictEqual(Error(expectedError)); + } + }); +}); diff --git a/lib/provider/io/jimp-image-reader.class.ts b/lib/provider/io/jimp-image-reader.class.ts index f33089be..1a0400a4 100644 --- a/lib/provider/io/jimp-image-reader.class.ts +++ b/lib/provider/io/jimp-image-reader.class.ts @@ -1,28 +1,39 @@ -import Jimp from 'jimp'; -import {ImageReader} from "../image-reader.type"; -import {Image} from "../../image.class"; -import {ColorMode} from "../../colormode.enum"; +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}`)); + 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 index 480cae06..aa4fc91e 100644 --- a/lib/provider/io/jimp-image-writer.class.spec.ts +++ b/lib/provider/io/jimp-image-writer.class.spec.ts @@ -1,43 +1,43 @@ import ImageWriter from "./jimp-image-writer.class"; -import {Image} from "../../image.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())) - } +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 - }) + 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(); +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}); + // 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 + // THEN + expect(scanMock).toHaveBeenCalledTimes(1); + expect(writeMock).toHaveBeenCalledTimes(1); + expect(writeMock).toHaveBeenCalledWith(outputFileName); + }); +}); diff --git a/lib/provider/io/jimp-image-writer.class.ts b/lib/provider/io/jimp-image-writer.class.ts index 664b7794..b80343f6 100644 --- a/lib/provider/io/jimp-image-writer.class.ts +++ b/lib/provider/io/jimp-image-writer.class.ts @@ -1,14 +1,14 @@ -import {ImageWriter, ImageWriterParameters} from "../image-writer.type"; -import {imageToJimp} from "./imageToJimp.function"; +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)); - }); - } + 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/clipboardy-clipboard.class.spec.ts b/lib/provider/native/clipboardy-clipboard.class.spec.ts index 9e96a795..1794d053 100644 --- a/lib/provider/native/clipboardy-clipboard.class.spec.ts +++ b/lib/provider/native/clipboardy-clipboard.class.spec.ts @@ -1,6 +1,6 @@ import ClipboardAction from "./clipboardy-clipboard.class"; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); beforeEach(() => { jest.resetAllMocks(); diff --git a/lib/provider/native/clipboardy-clipboard.class.ts b/lib/provider/native/clipboardy-clipboard.class.ts index a8ab2747..e3254ba9 100644 --- a/lib/provider/native/clipboardy-clipboard.class.ts +++ b/lib/provider/native/clipboardy-clipboard.class.ts @@ -2,8 +2,7 @@ import clippy from "clipboardy"; import { ClipboardProviderInterface } from "../clipboard-provider.interface"; export default class implements ClipboardProviderInterface { - constructor() { - } + constructor() {} public async hasText(): Promise { return new Promise((resolve, reject) => { diff --git a/lib/provider/native/libnut-keyboard.class.spec.ts b/lib/provider/native/libnut-keyboard.class.spec.ts index c1b44a19..15a27fd7 100644 --- a/lib/provider/native/libnut-keyboard.class.spec.ts +++ b/lib/provider/native/libnut-keyboard.class.spec.ts @@ -84,7 +84,11 @@ describe("libnut keyboard action", () => { // THEN expect(libnut.keyToggle).toBeCalledTimes(1); - expect(libnut.keyToggle).toBeCalledWith(KeyboardAction.keyLookup(Key.A), "down", []); + expect(libnut.keyToggle).toBeCalledWith( + KeyboardAction.keyLookup(Key.A), + "down", + [] + ); }); it("should treat a list of keys as modifiers + the actual key to press", async () => { @@ -96,8 +100,11 @@ describe("libnut keyboard action", () => { // THEN expect(libnut.keyToggle).toBeCalledTimes(1); - expect(libnut.keyToggle) - .toBeCalledWith(KeyboardAction.keyLookup(Key.A), "down", [KeyboardAction.keyLookup(Key.LeftControl)]); + expect(libnut.keyToggle).toBeCalledWith( + KeyboardAction.keyLookup(Key.A), + "down", + [KeyboardAction.keyLookup(Key.LeftControl)] + ); }); it("should not forward the pressKey call to libnut for an unknown key", async () => { @@ -135,7 +142,11 @@ describe("libnut keyboard action", () => { // THEN expect(libnut.keyToggle).toBeCalledTimes(1); - expect(libnut.keyToggle).toBeCalledWith(KeyboardAction.keyLookup(Key.A), "up", []); + expect(libnut.keyToggle).toBeCalledWith( + KeyboardAction.keyLookup(Key.A), + "up", + [] + ); }); it("should treat a list of keys as modifiers + the actual key to release", async () => { @@ -147,8 +158,11 @@ describe("libnut keyboard action", () => { // THEN expect(libnut.keyToggle).toBeCalledTimes(1); - expect(libnut.keyToggle) - .toBeCalledWith(KeyboardAction.keyLookup(Key.A), "up", [KeyboardAction.keyLookup(Key.LeftControl)]); + expect(libnut.keyToggle).toBeCalledWith( + KeyboardAction.keyLookup(Key.A), + "up", + [KeyboardAction.keyLookup(Key.LeftControl)] + ); }); it("should not forward the releaseKey call to libnut for an unknown key", async () => { diff --git a/lib/provider/native/libnut-keyboard.class.ts b/lib/provider/native/libnut-keyboard.class.ts index 920d2a9b..e16e1790 100644 --- a/lib/provider/native/libnut-keyboard.class.ts +++ b/lib/provider/native/libnut-keyboard.class.ts @@ -1,232 +1,234 @@ import libnut = require("@nut-tree/libnut"); -import {Key} from "../../key.enum"; -import {KeyboardProviderInterface} from "../keyboard-provider.interface"; +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.Decimal, "numpad_decimal"], - - [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, "add"], - [Key.Subtract, "subtract"], - [Key.Multiply, "multiply"], - [Key.Divide, "divide"], - [Key.Enter, "enter"], - - [Key.CapsLock, "caps_lock"], - [Key.ScrollLock, "scroll_lock"], - [Key.NumLock, "num_lock"], - - [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); - } + 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.Decimal, "numpad_decimal"], + + [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, "add"], + [Key.Subtract, "subtract"], + [Key.Multiply, "multiply"], + [Key.Divide, "divide"], + [Key.Enter, "enter"], + + [Key.CapsLock, "caps_lock"], + [Key.ScrollLock, "scroll_lock"], + [Key.NumLock, "num_lock"], + + [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.class.spec.ts b/lib/provider/native/libnut-mouse.class.spec.ts index cd74c31e..e7f445d6 100644 --- a/lib/provider/native/libnut-mouse.class.spec.ts +++ b/lib/provider/native/libnut-mouse.class.spec.ts @@ -269,7 +269,7 @@ describe("libnut mouse action", () => { describe("currentMousePosition", () => { it("should return the current mouse position via libnut", async () => { // GIVEN - libnut.getMousePos = jest.fn(() => ({x: 10, y: 100})); + libnut.getMousePos = jest.fn(() => ({ x: 10, y: 100 })); const SUT = new MouseAction(); // WHEN @@ -319,7 +319,9 @@ describe("libnut mouse action", () => { // WHEN // THEN - expect(SUT.setMousePosition(new Point(100, 100))).rejects.toThrowError(error); + expect(SUT.setMousePosition(new Point(100, 100))).rejects.toThrowError( + error + ); }); }); }); diff --git a/lib/provider/native/libnut-mouse.class.ts b/lib/provider/native/libnut-mouse.class.ts index 11ff2027..f5edf945 100644 --- a/lib/provider/native/libnut-mouse.class.ts +++ b/lib/provider/native/libnut-mouse.class.ts @@ -1,7 +1,7 @@ import libnut = require("@nut-tree/libnut"); -import {Button} from "../../button.enum"; -import {Point} from "../../point.class"; -import {MouseProviderInterface} from "../mouse-provider.interface"; +import { Button } from "../../button.enum"; +import { Point } from "../../point.class"; +import { MouseProviderInterface } from "../mouse-provider.interface"; export default class MouseAction implements MouseProviderInterface { public static buttonLookup(btn: Button): any { @@ -9,58 +9,61 @@ export default class MouseAction implements MouseProviderInterface { } private static ButtonLookupMap: Map = new Map( - [[Button.LEFT, "left"], [Button.MIDDLE, "middle"], [Button.RIGHT, "right"]], + [ + [Button.LEFT, "left"], + [Button.MIDDLE, "middle"], + [Button.RIGHT, "right"], + ] ); - constructor() { - } + constructor() {} public setMouseDelay(delay: number): void { libnut.setMouseDelay(delay); } public setMousePosition(p: Point): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.moveMouse(p.x, p.y); resolve(); } catch (e) { reject(e); } - })); + }); } public currentMousePosition(): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { const position = libnut.getMousePos(); resolve(new Point(position.x, position.y)); } catch (e) { reject(e); } - })); + }); } public click(btn: Button): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.mouseClick(MouseAction.buttonLookup(btn)); resolve(); } catch (e) { reject(e); } - })); + }); } public doubleClick(btn: Button): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.mouseClick(MouseAction.buttonLookup(btn), true); resolve(); } catch (e) { reject(e); } - })); + }); } public leftClick(): Promise { @@ -76,68 +79,68 @@ export default class MouseAction implements MouseProviderInterface { } public pressButton(btn: Button): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.mouseToggle("down", MouseAction.buttonLookup(btn)); resolve(); } catch (e) { reject(e); } - })); + }); } public releaseButton(btn: Button): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.mouseToggle("up", MouseAction.buttonLookup(btn)); resolve(); } catch (e) { reject(e); } - })); + }); } public scrollUp(amount: number): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.scrollMouse(0, amount); resolve(); } catch (e) { reject(e); } - })); + }); } public scrollDown(amount: number): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.scrollMouse(0, -amount); resolve(); } catch (e) { reject(e); } - })); + }); } public scrollLeft(amount: number): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.scrollMouse(-amount, 0); resolve(); } catch (e) { reject(e); } - })); + }); } public scrollRight(amount: number): Promise { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { try { libnut.scrollMouse(amount, 0); resolve(); } catch (e) { reject(e); } - })); + }); } } diff --git a/lib/provider/native/libnut-screen.class.spec.ts b/lib/provider/native/libnut-screen.class.spec.ts index 2e1aaa89..52d48b2f 100644 --- a/lib/provider/native/libnut-screen.class.spec.ts +++ b/lib/provider/native/libnut-screen.class.spec.ts @@ -30,20 +30,18 @@ describe("libnut screen action", () => { // GIVEN const SUT = new ScreenAction(); libnut.screen.capture = jest.fn(() => ({ - bitsPerPixel: 0, - byteWidth: 0, - bytesPerPixel: 0, - colorAt: jest.fn(), - height: screenShotSize.height, - image: new ArrayBuffer(0), - width: screenShotSize.width, - }) - ); + bitsPerPixel: 0, + byteWidth: 0, + bytesPerPixel: 0, + colorAt: jest.fn(), + height: screenShotSize.height, + image: new ArrayBuffer(0), + width: screenShotSize.width, + })); libnut.getScreenSize = jest.fn(() => ({ - height: screenSize.height, - width: screenSize.width, - }) - ); + height: screenSize.height, + width: screenSize.width, + })); // WHEN const image = await SUT.grabScreen(); @@ -61,15 +59,14 @@ describe("libnut screen action", () => { const screenRegion = new Region(0, 0, 10, 10); const SUT = new ScreenAction(); libnut.screen.capture = jest.fn(() => ({ - bitsPerPixel: 0, - byteWidth: 0, - bytesPerPixel: 0, - colorAt: jest.fn(), - height: screenShotSize.height, - image: new ArrayBuffer(0), - width: screenShotSize.width, - }) - ); + bitsPerPixel: 0, + byteWidth: 0, + bytesPerPixel: 0, + colorAt: jest.fn(), + height: screenShotSize.height, + image: new ArrayBuffer(0), + width: screenShotSize.width, + })); // WHEN const image = await SUT.grabScreenRegion(screenRegion); @@ -91,10 +88,16 @@ describe("libnut screen action", () => { const screenRegion = new Region(0, 0, 10, 10); // THEN - await expect(call(screenRegion)).rejects.toEqual("Unable to fetch screen content."); + await expect(call(screenRegion)).rejects.toEqual( + "Unable to fetch screen content." + ); expect(libnut.screen.capture).toBeCalledTimes(1); - expect(libnut.screen.capture) - .toBeCalledWith(screenRegion.left, screenRegion.top, screenRegion.width, screenRegion.height); + expect(libnut.screen.capture).toBeCalledWith( + screenRegion.left, + screenRegion.top, + screenRegion.width, + screenRegion.height + ); }); }); @@ -103,7 +106,10 @@ describe("libnut screen action", () => { it("should determine screen width via libnut", async () => { // GIVEN const SUT = new ScreenAction(); - libnut.getScreenSize = jest.fn(() => ({width: screenSize.width, height: screenSize.height})); + libnut.getScreenSize = jest.fn(() => ({ + width: screenSize.width, + height: screenSize.height, + })); // WHEN const width = await SUT.screenWidth(); @@ -131,7 +137,10 @@ describe("libnut screen action", () => { it("should determine screen height via libnut", async () => { // GIVEN const SUT = new ScreenAction(); - libnut.getScreenSize = jest.fn(() => ({width: screenSize.width, height: screenSize.height})); + libnut.getScreenSize = jest.fn(() => ({ + width: screenSize.width, + height: screenSize.height, + })); // WHEN const width = await SUT.screenHeight(); @@ -159,7 +168,10 @@ describe("libnut screen action", () => { it("should determine screen size via libnut", async () => { // GIVEN const SUT = new ScreenAction(); - libnut.getScreenSize = jest.fn(() => ({width: screenSize.width, height: screenSize.height})); + libnut.getScreenSize = jest.fn(() => ({ + width: screenSize.width, + height: screenSize.height, + })); // WHEN const size = await SUT.screenSize(); @@ -197,11 +209,22 @@ describe("libnut screen action", () => { const highlightOpacity = 1.0; // WHEN - await SUT.highlightScreenRegion(testRegion, highlightDuration, highlightOpacity); + await SUT.highlightScreenRegion( + testRegion, + highlightDuration, + highlightOpacity + ); // THEN expect(libnut.screen.highlight).toBeCalledTimes(1); - expect(libnut.screen.highlight).toBeCalledWith(x, y, w, h, highlightDuration, highlightOpacity); + expect(libnut.screen.highlight).toBeCalledWith( + x, + y, + w, + h, + highlightDuration, + highlightOpacity + ); }); it("should reject on libnut errors", async () => { diff --git a/lib/provider/native/libnut-screen.class.ts b/lib/provider/native/libnut-screen.class.ts index 1ab716a5..144a2dc7 100644 --- a/lib/provider/native/libnut-screen.class.ts +++ b/lib/provider/native/libnut-screen.class.ts @@ -1,14 +1,13 @@ import libnut = require("@nut-tree/libnut"); -import {Image} from "../../image.class"; -import {Region} from "../../region.class"; -import {ScreenProviderInterface} from "../screen-provider.interface"; -import {ColorMode} from "../../colormode.enum"; +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 ScreenProviderInterface { - private static determinePixelDensity( screen: Region, - screenShot: libnut.Bitmap, + screenShot: libnut.Bitmap ): { scaleX: number; scaleY: number } { return { scaleX: screenShot.width / screen.width, @@ -16,8 +15,7 @@ export default class ScreenAction implements ScreenProviderInterface { }; } - constructor() { - } + constructor() {} public grabScreen(): Promise { return new Promise((resolve, reject) => { @@ -26,7 +24,7 @@ export default class ScreenAction implements ScreenProviderInterface { const screenSize = libnut.getScreenSize(); const pixelScaling = ScreenAction.determinePixelDensity( new Region(0, 0, screenSize.width, screenSize.height), - screenShot, + screenShot ); resolve( new Image( @@ -34,10 +32,10 @@ export default class ScreenAction implements ScreenProviderInterface { screenShot.height, screenShot.image, 4, - "grabScreenResult", + "grabScreenResult", ColorMode.BGR, - pixelScaling, - ), + pixelScaling + ) ); } else { reject("Unable to fetch screen content."); @@ -51,10 +49,13 @@ export default class ScreenAction implements ScreenProviderInterface { region.left, region.top, region.width, - region.height, + region.height ); if (screenShot) { - const pixelScaling = ScreenAction.determinePixelDensity(region, screenShot); + const pixelScaling = ScreenAction.determinePixelDensity( + region, + screenShot + ); resolve( new Image( screenShot.width, @@ -63,8 +64,8 @@ export default class ScreenAction implements ScreenProviderInterface { 4, "grabScreenRegionResult", ColorMode.BGR, - pixelScaling, - ), + pixelScaling + ) ); } else { reject("Unable to fetch screen content."); @@ -72,14 +73,24 @@ export default class ScreenAction implements ScreenProviderInterface { }); } - public highlightScreenRegion(region: Region, duration: number, opacity: number): Promise { + public highlightScreenRegion( + region: Region, + duration: number, + opacity: number + ): Promise { return new Promise((resolve) => { - libnut.screen.highlight(region.left, region.top, region.width, region.height, duration, opacity); + libnut.screen.highlight( + region.left, + region.top, + region.width, + region.height, + duration, + opacity + ); resolve(); }); } - public screenWidth(): Promise { return new Promise((resolve, reject) => { try { diff --git a/lib/provider/native/libnut-window.class.spec.ts b/lib/provider/native/libnut-window.class.spec.ts index a89a9deb..36efd48b 100644 --- a/lib/provider/native/libnut-window.class.spec.ts +++ b/lib/provider/native/libnut-window.class.spec.ts @@ -1,153 +1,158 @@ import libnut = require("@nut-tree/libnut"); import WindowAction from "./libnut-window.class"; -import {Region} from "../../region.class"; +import { Region } from "../../region.class"; jest.mock("@nut-tree/libnut"); beforeEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks(); }); describe("libnut WindowAction", () => { - describe("getWindows", () => { - it("should resolve to a list of numeric window handles via libnut#getWindows", async () => { - // GIVEN - const SUT = new WindowAction(); - const windowList = [1,2,3]; - libnut.getWindows = jest.fn(() => windowList); - - // WHEN - const windows = SUT.getWindows(); - - // THEN - await expect(libnut.getWindows).toBeCalledTimes(1); - await expect(windows).resolves.toBe(windowList); - }); - - it("should reject on errors in libnut#getWindows", async () => { - // GIVEN - const SUT = new WindowAction(); - const errorMessage = "getWindows threw"; - libnut.getWindows = jest.fn(() => { - throw new Error(errorMessage); - }); - - // WHEN - const windows = SUT.getWindows(); - - // THEN - await expect(libnut.getWindows).toBeCalledTimes(1); - await expect(windows).rejects.toThrowError(errorMessage); - }); + describe("getWindows", () => { + it("should resolve to a list of numeric window handles via libnut#getWindows", async () => { + // GIVEN + const SUT = new WindowAction(); + const windowList = [1, 2, 3]; + libnut.getWindows = jest.fn(() => windowList); + + // WHEN + const windows = SUT.getWindows(); + + // THEN + await expect(libnut.getWindows).toBeCalledTimes(1); + await expect(windows).resolves.toBe(windowList); }); - describe("getActiveWindow", () => { - it("should resolve to a numeric window handles via libnut#getActiveWindow", async () => { - // GIVEN - const SUT = new WindowAction(); - const activeWindow = 1; - libnut.getActiveWindow = jest.fn(() => activeWindow); - - // WHEN - const window = SUT.getActiveWindow(); - - // THEN - await expect(libnut.getActiveWindow).toBeCalledTimes(1); - await expect(window).resolves.toBe(activeWindow); - }); - - it("should reject on errors in libnut#getActiveWindow", async () => { - // GIVEN - const SUT = new WindowAction(); - const errorMessage = "getActiveWindow threw"; - libnut.getActiveWindow = jest.fn(() => { - throw new Error(errorMessage); - }); - - // WHEN - const windows = SUT.getActiveWindow(); - - // THEN - await expect(libnut.getActiveWindow).toBeCalledTimes(1); - await expect(windows).rejects.toThrowError(errorMessage); - }); + it("should reject on errors in libnut#getWindows", async () => { + // GIVEN + const SUT = new WindowAction(); + const errorMessage = "getWindows threw"; + libnut.getWindows = jest.fn(() => { + throw new Error(errorMessage); + }); + + // WHEN + const windows = SUT.getWindows(); + + // THEN + await expect(libnut.getWindows).toBeCalledTimes(1); + await expect(windows).rejects.toThrowError(errorMessage); }); + }); + + describe("getActiveWindow", () => { + it("should resolve to a numeric window handles via libnut#getActiveWindow", async () => { + // GIVEN + const SUT = new WindowAction(); + const activeWindow = 1; + libnut.getActiveWindow = jest.fn(() => activeWindow); + + // WHEN + const window = SUT.getActiveWindow(); - describe("getWindowRegion", () => { - it("should resolve to a window region via libnut#getWindowRegion", async () => { - // GIVEN - const SUT = new WindowAction(); - const windowHandle = 100; - const windowRect = { - x: 1, - y: 2, - width: 42, - height: 23 - }; - const windowRegion = new Region(windowRect.x, windowRect.y, windowRect.width, windowRect.height); - libnut.getWindowRect = jest.fn(() => windowRect); - - // WHEN - const wndRegion = SUT.getWindowRegion(windowHandle); - - // THEN - await expect(libnut.getWindowRect).toBeCalledTimes(1); - await expect(libnut.getWindowRect).toBeCalledWith(windowHandle); - await expect(wndRegion).resolves.toStrictEqual(windowRegion); - }); - - it("should reject on errors in libnut#getActiveWindow", async () => { - // GIVEN - const SUT = new WindowAction(); - const errorMessage = "getWindowRect threw"; - const windowHandle = 100; - libnut.getWindowRect = jest.fn(() => { - throw new Error(errorMessage); - }); - - // WHEN - const windows = SUT.getWindowRegion(windowHandle); - - // THEN - await expect(libnut.getWindowRect).toBeCalledTimes(1); - await expect(libnut.getWindowRect).toBeCalledWith(windowHandle); - await expect(windows).rejects.toThrowError(errorMessage); - }); + // THEN + await expect(libnut.getActiveWindow).toBeCalledTimes(1); + await expect(window).resolves.toBe(activeWindow); }); - describe("getWindowTitle", () => { - it("should resolve to a window title via libnut#getWindowTitle", async () => { - // GIVEN - const SUT = new WindowAction(); - const windowTitle = "test window"; - const windowHandle = 42; - libnut.getWindowTitle = jest.fn(() => windowTitle); - - // WHEN - const wndRegion = SUT.getWindowTitle(windowHandle); - - // THEN - await expect(libnut.getWindowTitle).toBeCalledTimes(1); - await expect(libnut.getWindowTitle).toBeCalledWith(windowHandle); - await expect(wndRegion).resolves.toBe(windowTitle); - }); - - it("should reject on errors in libnut#getActiveWindow", async () => { - // GIVEN - const SUT = new WindowAction(); - const errorMessage = "getWindowRect threw"; - const windowHandle = 42; - libnut.getWindowTitle = jest.fn(() => { - throw new Error(errorMessage); - }); - - // WHEN - const windows = SUT.getWindowTitle(windowHandle); - - // THEN - await expect(libnut.getWindowTitle).toBeCalledTimes(1); - await expect(libnut.getWindowTitle).toBeCalledWith(windowHandle); - await expect(windows).rejects.toThrowError(errorMessage); - }); + it("should reject on errors in libnut#getActiveWindow", async () => { + // GIVEN + const SUT = new WindowAction(); + const errorMessage = "getActiveWindow threw"; + libnut.getActiveWindow = jest.fn(() => { + throw new Error(errorMessage); + }); + + // WHEN + const windows = SUT.getActiveWindow(); + + // THEN + await expect(libnut.getActiveWindow).toBeCalledTimes(1); + await expect(windows).rejects.toThrowError(errorMessage); + }); + }); + + describe("getWindowRegion", () => { + it("should resolve to a window region via libnut#getWindowRegion", async () => { + // GIVEN + const SUT = new WindowAction(); + const windowHandle = 100; + const windowRect = { + x: 1, + y: 2, + width: 42, + height: 23, + }; + const windowRegion = new Region( + windowRect.x, + windowRect.y, + windowRect.width, + windowRect.height + ); + libnut.getWindowRect = jest.fn(() => windowRect); + + // WHEN + const wndRegion = SUT.getWindowRegion(windowHandle); + + // THEN + await expect(libnut.getWindowRect).toBeCalledTimes(1); + await expect(libnut.getWindowRect).toBeCalledWith(windowHandle); + await expect(wndRegion).resolves.toStrictEqual(windowRegion); }); -}); \ No newline at end of file + + it("should reject on errors in libnut#getActiveWindow", async () => { + // GIVEN + const SUT = new WindowAction(); + const errorMessage = "getWindowRect threw"; + const windowHandle = 100; + libnut.getWindowRect = jest.fn(() => { + throw new Error(errorMessage); + }); + + // WHEN + const windows = SUT.getWindowRegion(windowHandle); + + // THEN + await expect(libnut.getWindowRect).toBeCalledTimes(1); + await expect(libnut.getWindowRect).toBeCalledWith(windowHandle); + await expect(windows).rejects.toThrowError(errorMessage); + }); + }); + + describe("getWindowTitle", () => { + it("should resolve to a window title via libnut#getWindowTitle", async () => { + // GIVEN + const SUT = new WindowAction(); + const windowTitle = "test window"; + const windowHandle = 42; + libnut.getWindowTitle = jest.fn(() => windowTitle); + + // WHEN + const wndRegion = SUT.getWindowTitle(windowHandle); + + // THEN + await expect(libnut.getWindowTitle).toBeCalledTimes(1); + await expect(libnut.getWindowTitle).toBeCalledWith(windowHandle); + await expect(wndRegion).resolves.toBe(windowTitle); + }); + + it("should reject on errors in libnut#getActiveWindow", async () => { + // GIVEN + const SUT = new WindowAction(); + const errorMessage = "getWindowRect threw"; + const windowHandle = 42; + libnut.getWindowTitle = jest.fn(() => { + throw new Error(errorMessage); + }); + + // WHEN + const windows = SUT.getWindowTitle(windowHandle); + + // THEN + await expect(libnut.getWindowTitle).toBeCalledTimes(1); + await expect(libnut.getWindowTitle).toBeCalledWith(windowHandle); + await expect(windows).rejects.toThrowError(errorMessage); + }); + }); +}); diff --git a/lib/provider/native/libnut-window.class.ts b/lib/provider/native/libnut-window.class.ts index 40804364..894ffa3a 100644 --- a/lib/provider/native/libnut-window.class.ts +++ b/lib/provider/native/libnut-window.class.ts @@ -27,7 +27,14 @@ export default class WindowAction implements WindowProviderInterface { return new Promise((resolve, reject) => { try { const windowRect = libnut.getWindowRect(windowHandle); - resolve(new Region(windowRect.x, windowRect.y, windowRect.width, windowRect.height)); + resolve( + new Region( + windowRect.x, + windowRect.y, + windowRect.width, + windowRect.height + ) + ); } catch (e) { reject(e); } diff --git a/lib/provider/provider-registry.class.ts b/lib/provider/provider-registry.class.ts index 17aa1c1f..635fedb3 100644 --- a/lib/provider/provider-registry.class.ts +++ b/lib/provider/provider-registry.class.ts @@ -1,170 +1,170 @@ -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 { 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 { 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; + getClipboard(): ClipboardProviderInterface; - registerClipboardProvider(value: ClipboardProviderInterface): void; + registerClipboardProvider(value: ClipboardProviderInterface): void; - getKeyboard(): KeyboardProviderInterface; + getKeyboard(): KeyboardProviderInterface; - registerKeyboardProvider(value: KeyboardProviderInterface): void; + registerKeyboardProvider(value: KeyboardProviderInterface): void; - getMouse(): MouseProviderInterface; + getMouse(): MouseProviderInterface; - registerMouseProvider(value: MouseProviderInterface): void; + registerMouseProvider(value: MouseProviderInterface): void; - getScreen(): ScreenProviderInterface; + getScreen(): ScreenProviderInterface; - registerScreenProvider(value: ScreenProviderInterface): void; + registerScreenProvider(value: ScreenProviderInterface): void; - getWindow(): WindowProviderInterface; + getWindow(): WindowProviderInterface; - registerWindowProvider(value: WindowProviderInterface): void; + registerWindowProvider(value: WindowProviderInterface): void; - getImageFinder(): ImageFinderInterface; + getImageFinder(): ImageFinderInterface; - registerImageFinder(value: ImageFinderInterface): void; + registerImageFinder(value: ImageFinderInterface): void; - getImageReader(): ImageReader; + getImageReader(): ImageReader; - registerImageReader(value: ImageReader): void; + registerImageReader(value: ImageReader): void; - getImageWriter(): ImageWriter; + getImageWriter(): ImageWriter; - registerImageWriter(value: ImageWriter): void; + registerImageWriter(value: ImageWriter): void; - getImageProcessor(): ImageProcessor; + getImageProcessor(): ImageProcessor; - registerImageProcessor(value: ImageProcessor): void; + 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`); - } + 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; - registerClipboardProvider(value: ClipboardProviderInterface) { - this._clipboard = value; + getClipboard(): ClipboardProviderInterface { + if (this._clipboard) { + return this._clipboard; } + throw new Error(`No ClipboardProvider registered`); + } - getImageFinder(): ImageFinderInterface { - if (this._finder) { - return this._finder; - } - throw new Error(`No ImageFinder registered`); - } + registerClipboardProvider(value: ClipboardProviderInterface) { + this._clipboard = value; + } - registerImageFinder(value: ImageFinderInterface) { - this._finder = value; + getImageFinder(): ImageFinderInterface { + if (this._finder) { + return this._finder; } + throw new Error(`No ImageFinder registered`); + } - getKeyboard(): KeyboardProviderInterface { - if (this._keyboard) { - return this._keyboard; - } - throw new Error(`No KeyboardProvider registered`); - } + registerImageFinder(value: ImageFinderInterface) { + this._finder = value; + } - registerKeyboardProvider(value: KeyboardProviderInterface) { - this._keyboard = value; + getKeyboard(): KeyboardProviderInterface { + if (this._keyboard) { + return this._keyboard; } + throw new Error(`No KeyboardProvider registered`); + } - getMouse(): MouseProviderInterface { - if (this._mouse) { - return this._mouse; - } - throw new Error(`No MouseProvider registered`); - } + registerKeyboardProvider(value: KeyboardProviderInterface) { + this._keyboard = value; + } - registerMouseProvider(value: MouseProviderInterface) { - this._mouse = value; + getMouse(): MouseProviderInterface { + if (this._mouse) { + return this._mouse; } + throw new Error(`No MouseProvider registered`); + } - getScreen(): ScreenProviderInterface { - if (this._screen) { - return this._screen; - } - throw new Error(`No ScreenProvider registered`); - } + registerMouseProvider(value: MouseProviderInterface) { + this._mouse = value; + } - registerScreenProvider(value: ScreenProviderInterface) { - this._screen = value; + getScreen(): ScreenProviderInterface { + if (this._screen) { + return this._screen; } + throw new Error(`No ScreenProvider registered`); + } - getWindow(): WindowProviderInterface { - if (this._window) { - return this._window; - } - throw new Error(`No WindowProvider registered`); - } + registerScreenProvider(value: ScreenProviderInterface) { + this._screen = value; + } - registerWindowProvider(value: WindowProviderInterface) { - this._window = value; + getWindow(): WindowProviderInterface { + if (this._window) { + return this._window; } + throw new Error(`No WindowProvider registered`); + } - getImageReader(): ImageReader { - if (this._imageReader) { - return this._imageReader; - } - throw new Error(`No ImageReader registered`); - } + registerWindowProvider(value: WindowProviderInterface) { + this._window = value; + } - registerImageReader(value: ImageReader) { - this._imageReader = value; + getImageReader(): ImageReader { + if (this._imageReader) { + return this._imageReader; } + throw new Error(`No ImageReader registered`); + } - getImageWriter(): ImageWriter { - if (this._imageWriter) { - return this._imageWriter; - } - throw new Error(`No ImageWriter registered`); - } + registerImageReader(value: ImageReader) { + this._imageReader = value; + } - registerImageWriter(value: ImageWriter) { - this._imageWriter = value; + getImageWriter(): ImageWriter { + if (this._imageWriter) { + return this._imageWriter; } + throw new Error(`No ImageWriter registered`); + } - getImageProcessor(): ImageProcessor { - if (this._imageProcessor) { - return this._imageProcessor; - } - throw new Error(`No ImageProcessor registered`); - } + registerImageWriter(value: ImageWriter) { + this._imageWriter = value; + } - registerImageProcessor(value: ImageProcessor): void { - this._imageProcessor = 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(); @@ -178,4 +178,4 @@ providerRegistry.registerImageWriter(new ImageWriterImpl()); providerRegistry.registerImageReader(new ImageReaderImpl()); providerRegistry.registerImageProcessor(new ImageProcessorImpl()); -export default providerRegistry; \ No newline at end of file +export default providerRegistry; diff --git a/lib/provider/screen-provider.interface.ts b/lib/provider/screen-provider.interface.ts index 367ad65f..766307fd 100644 --- a/lib/provider/screen-provider.interface.ts +++ b/lib/provider/screen-provider.interface.ts @@ -30,7 +30,11 @@ export interface ScreenProviderInterface { * @param duration The highlight duration * @param opacity Overlay opacity */ - highlightScreenRegion(region: Region, duration: number, opacity: number): Promise; + highlightScreenRegion( + region: Region, + duration: number, + opacity: number + ): Promise; /** * screenWidth returns a systems main screen width diff --git a/lib/region.class.spec.ts b/lib/region.class.spec.ts index 4e2dda5d..6a02d5c5 100644 --- a/lib/region.class.spec.ts +++ b/lib/region.class.spec.ts @@ -1,4 +1,4 @@ -import {isRegion, Region} from "./region.class"; +import { isRegion, Region } from "./region.class"; describe("Region", () => { it("should calculate the correct area of a region", () => { @@ -15,8 +15,8 @@ describe("Region", () => { expect(region.toString()).toEqual(expected); }); - describe('isRegion typeguard', () => { - it('should identify a Region', () => { + describe("isRegion typeguard", () => { + it("should identify a Region", () => { // GIVEN const r = new Region(100, 100, 100, 100); @@ -27,7 +27,7 @@ describe("Region", () => { expect(result).toBeTruthy(); }); - it('should rule out non-objects', () => { + it("should rule out non-objects", () => { // GIVEN const r = "foo"; @@ -38,7 +38,7 @@ describe("Region", () => { expect(result).toBeFalsy(); }); - it('should rule out possible object with missing properties', () => { + it("should rule out possible object with missing properties", () => { // GIVEN const r = { left: 100, @@ -53,13 +53,13 @@ describe("Region", () => { expect(result).toBeFalsy(); }); - it('should rule out possible object with wrong property type', () => { + it("should rule out possible object with wrong property type", () => { // GIVEN const r = { left: 100, - top: 'foo', + top: "foo", width: 100, - height: 200 + height: 200, }; // WHEN @@ -68,5 +68,5 @@ describe("Region", () => { // THEN expect(result).toBeFalsy(); }); - }) + }); }); diff --git a/lib/region.class.ts b/lib/region.class.ts index 742f5ae5..edf154c9 100644 --- a/lib/region.class.ts +++ b/lib/region.class.ts @@ -1,37 +1,36 @@ export class Region { - constructor( - public left: number, - public top: number, - public width: number, - public height: number, - ) { - } + constructor( + public left: number, + public top: number, + public width: number, + public height: number + ) {} - public area() { - return this.width * this.height; - } + public area() { + return this.width * this.height; + } - public toString() { - return `(${this.left}, ${this.top}, ${this.width}, ${this.height})`; - } + public toString() { + return `(${this.left}, ${this.top}, ${this.width}, ${this.height})`; + } } const testRegion = new Region(0, 0, 100, 100); const regionKeys = Object.keys(testRegion); export function isRegion(possibleRegion: any): possibleRegion is Region { - if (typeof possibleRegion !== 'object') { - return false; + if (typeof possibleRegion !== "object") { + return false; + } + for (const key of regionKeys) { + if (!(key in possibleRegion)) { + return false; } - for (const key of regionKeys) { - if (!(key in possibleRegion)) { - return false; - } - const possibleRegionKeyType = typeof possibleRegion[key]; - const regionKeyType = typeof testRegion[key as keyof typeof testRegion]; - if (possibleRegionKeyType !== regionKeyType) { - return false - } + const possibleRegionKeyType = typeof possibleRegion[key]; + const regionKeyType = typeof testRegion[key as keyof typeof testRegion]; + if (possibleRegionKeyType !== regionKeyType) { + return false; } - return true; + } + return true; } diff --git a/lib/rgba.class.ts b/lib/rgba.class.ts index 9e40a245..95f0df32 100644 --- a/lib/rgba.class.ts +++ b/lib/rgba.class.ts @@ -1,13 +1,20 @@ export class RGBA { - constructor(public readonly R: number, public readonly G: number, public readonly B: number, public readonly A: number) { - } + 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 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')}` - } + 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/scaled-match-result.class.ts b/lib/scaled-match-result.class.ts index 85db56ca..1b7d055b 100644 --- a/lib/scaled-match-result.class.ts +++ b/lib/scaled-match-result.class.ts @@ -2,10 +2,11 @@ import { MatchResult } from "./match-result.class"; import { Region } from "./region.class"; export class ScaledMatchResult extends MatchResult { - constructor(public readonly confidence: number, - public readonly scale: number, - public readonly location: Region, - public readonly error?: Error + constructor( + public readonly confidence: number, + public readonly scale: number, + public readonly location: Region, + public readonly error?: Error ) { super(confidence, location, error); } diff --git a/lib/screen.class.spec.ts b/lib/screen.class.spec.ts index bfa23028..c8e5e17b 100644 --- a/lib/screen.class.spec.ts +++ b/lib/screen.class.spec.ts @@ -1,707 +1,836 @@ -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', () => { -}); +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); 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); - } - }) - } + 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); + }, + }); + }, }); beforeEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks(); }); describe("Screen.", () => { - describe("find", () => { - it("should resolve with sufficient confidence.", async () => { - // GIVEN - const matchResult = new MatchResult(0.99, searchRegion); - 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(needlePromise); - - // THEN - await expect(resultRegion).resolves.toEqual(matchResult.location); - const matchRequest = new MatchRequest( - expect.any(Image), - needle, - SUT.config.confidence, - true); - expect(findMatchMock).toHaveBeenCalledWith(matchRequest); - }); - - it("should call registered hook before resolve", async () => { - // GIVEN - const matchResult = new MatchResult(0.99, searchRegion); - const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); - providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ - findMatch: findMatchMock - })); - - 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.find(needle); - - // THEN - expect(testCallback).toBeCalledTimes(1); - expect(testCallback).toBeCalledWith(matchResult); - }); - - it("should call multiple registered hooks before resolve", async () => { - // GIVEN - const matchResult = new MatchResult(0.99, searchRegion); - const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); - providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ - findMatch: findMatchMock - })); - - 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.find(needle); - - // THEN - for (const callback of [testCallback, secondCallback]) { - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith(matchResult); - } - }); - - it("should reject with insufficient confidence.", async () => { - - // GIVEN - 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(needle, {confidence: minConfidence}); - - // THEN - await expect(resultRegion) - .rejects - .toEqual(`Searching for ${id} failed. Reason: '${expectedReason}'`); - }); - - it("should reject when search fails.", async () => { - - // GIVEN - const rejectionReason = "Search failed."; - const findMatchMock = jest.fn(() => Promise.reject(rejectionReason)); - 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(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 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(undefined, minMatch); - - // WHEN - const resultRegion = SUT.find(needle, parameters); - - // THEN - await expect(resultRegion).resolves.toEqual(matchResult.location); - const matchRequest = new MatchRequest( - expect.any(Image), - needle, - minMatch, - true); - 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); - - 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), - needle, - SUT.config.confidence, - true); - - // WHEN - await SUT.find(needle, parameters); - - // THEN - expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest); - }); - - it("should override searchMultipleScales with parameter.", async () => { - // GIVEN - const matchResult = new MatchResult(0.99, searchRegion); - 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), - needle, - SUT.config.confidence, - false); - - // WHEN - await SUT.find(needle, parameters); - - // THEN - expect(findMatchMock).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 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), - needle, - minMatch, - true); - - // WHEN - await SUT.find(needle, parameters); - - // THEN - 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); - const matchResult = new MatchResult(0.99, resultRegion); - - const expectedMatchRegion = new Region( - limitedSearchRegion.left + resultRegion.left, - limitedSearchRegion.top + resultRegion.top, - resultRegion.width, - resultRegion.height); - - 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( - new Image(100, 100, Buffer.from([]), 3, "needle_image"), - { - searchRegion: limitedSearchRegion - } - ); - - // THEN - expect(matchRegion).toEqual(expectedMatchRegion); + describe("find", () => { + it("should resolve with sufficient confidence.", async () => { + // GIVEN + const matchResult = new MatchResult(0.99, searchRegion); + 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(needlePromise); + + // THEN + await expect(resultRegion).resolves.toEqual(matchResult.location); + const matchRequest = new MatchRequest( + expect.any(Image), + needle, + SUT.config.confidence, + true + ); + expect(findMatchMock).toHaveBeenCalledWith(matchRequest); + }); + + it("should call registered hook before resolve", async () => { + // GIVEN + const matchResult = new MatchResult(0.99, searchRegion); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => + mockPartial({ + findMatch: findMatchMock, + }) + ); + + 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.find(needle); + + // THEN + expect(testCallback).toBeCalledTimes(1); + expect(testCallback).toBeCalledWith(matchResult); + }); + + it("should call multiple registered hooks before resolve", async () => { + // GIVEN + const matchResult = new MatchResult(0.99, searchRegion); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => + mockPartial({ + findMatch: findMatchMock, + }) + ); + + 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.find(needle); + + // THEN + for (const callback of [testCallback, secondCallback]) { + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith(matchResult); + } + }); + + it("should reject with insufficient confidence.", async () => { + // GIVEN + 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(needle, { confidence: minConfidence }); + + // THEN + await expect(resultRegion).rejects.toEqual( + `Searching for ${id} failed. Reason: '${expectedReason}'` + ); + }); + + it("should reject when search fails.", async () => { + // GIVEN + const rejectionReason = "Search failed."; + const findMatchMock = jest.fn(() => Promise.reject(rejectionReason)); + 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(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 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(undefined, minMatch); + + // WHEN + const resultRegion = SUT.find(needle, parameters); + + // THEN + await expect(resultRegion).resolves.toEqual(matchResult.location); + const matchRequest = new MatchRequest( + expect.any(Image), + needle, + minMatch, + true + ); + 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); + + 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), + needle, + SUT.config.confidence, + true + ); + + // WHEN + await SUT.find(needle, parameters); + + // THEN + expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest); + }); + + it("should override searchMultipleScales with parameter.", async () => { + // GIVEN + const matchResult = new MatchResult(0.99, searchRegion); + 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), + needle, + SUT.config.confidence, + false + ); + + // WHEN + await SUT.find(needle, parameters); + + // THEN + expect(findMatchMock).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 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), + needle, + minMatch, + true + ); + + // WHEN + await SUT.find(needle, parameters); + + // THEN + 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); + const matchResult = new MatchResult(0.99, resultRegion); + + const expectedMatchRegion = new Region( + limitedSearchRegion.left + resultRegion.left, + limitedSearchRegion.top + resultRegion.top, + resultRegion.width, + resultRegion.height + ); + + 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( + 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); + 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 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 ScreenClass(providerRegistryMock); - 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); - 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:`); + 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); }); - 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 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 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("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.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); - const findMatchesMock = jest.fn(() => Promise.resolve([matchResult])); - providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ - findMatches: findMatchesMock - })); - - const SUT = new ScreenClass(providerRegistryMock); - - // WHEN - const findPromise = SUT.findAll( - needle, - { - searchRegion: region - }); - - // THEN - await expect(findPromise).rejects.toContain(`Searching for ${id} failed. Reason:`); + 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("should return region to highlight for chaining", async () => { - // GIVEN - const highlightRegion = new Region(10, 20, 30, 40); - const highlightMock = jest.fn((value: any) => Promise.resolve(value)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - highlightScreenRegion: highlightMock - })); + 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); + const findMatchesMock = jest.fn(() => Promise.resolve([matchResult])); + providerRegistryMock.getImageFinder = jest.fn(() => + mockPartial({ + findMatches: findMatchesMock, + }) + ); - const SUT = new ScreenClass(providerRegistryMock); - // WHEN - const result = await SUT.highlight(highlightRegion); + const SUT = new ScreenClass(providerRegistryMock); - // THEN - expect(result).toEqual(highlightRegion); + // WHEN + const findPromise = SUT.findAll(needle, { + searchRegion: region, + }); + + // THEN + 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); + 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); + + // THEN + expect(result).toEqual(highlightRegion); + }); + + it("should handle Promises and return region to highlight for chaining", async () => { + // GIVEN + const highlightRegion = new Region(10, 20, 30, 40); + const highlightRegionPromise = new Promise((res) => + res(highlightRegion) + ); + 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); + + // THEN + expect(result).toEqual(highlightRegion); + }); + + describe("capture", () => { + it("should capture the whole screen and save image", async () => { + // GIVEN + const screenshot = new Image(100, 100, Buffer.from([]), 4, "test"); + 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(grabScreenMock).toHaveBeenCalled(); + expect(saveImageMock).toHaveBeenCalledWith(expectedData); }); - it("should handle Promises and return region to highlight for chaining", async () => { - // GIVEN - const highlightRegion = new Region(10, 20, 30, 40); - const highlightRegionPromise = new Promise(res => res(highlightRegion)); - const highlightMock = jest.fn((value: any) => Promise.resolve(value)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - highlightScreenRegion: highlightMock - })); + it("should throw in non-image input", async () => { + // GIVEN + const screenshot = mockPartial({ data: Buffer.from([]) }); + const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); + providerRegistryMock.getScreen = jest.fn(() => + mockPartial({ + grabScreen: grabScreenMock, + }) + ); - const SUT = new ScreenClass(providerRegistryMock); + const SUT = new ScreenClass(providerRegistryMock); + const imageName = "foobar.png"; - // WHEN - const result = await SUT.highlight(highlightRegionPromise); + // WHEN + const result = SUT.capture(imageName); - // THEN - expect(result).toEqual(highlightRegion); + // THEN + expect(result).rejects.toThrowError( + /^capture requires an Image, but received/ + ); + }); + }); + + describe("captureRegion", () => { + it("should capture the specified region of the screen and save image", async () => { + // GIVEN + const screenshot = new Image(100, 100, Buffer.from([]), 4, "test"); + 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(grabScreenMock).toHaveBeenCalledWith(regionToCapture); + expect(saveImageMock).toHaveBeenCalledWith(expectedData); }); - describe("capture", () => { - it("should capture the whole screen and save image", async () => { - // GIVEN - const screenshot = new Image(100, 100, Buffer.from([]), 4, "test"); - 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(grabScreenMock).toHaveBeenCalled() - expect(saveImageMock).toHaveBeenCalledWith(expectedData); - }); - - it("should throw in non-image input", async () => { - // GIVEN - const screenshot = mockPartial({data: Buffer.from([])}); - const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - grabScreen: grabScreenMock - })); - - const SUT = new ScreenClass(providerRegistryMock); - const imageName = "foobar.png" - - // WHEN - const result = SUT.capture(imageName ) - - // THEN - expect(result).rejects.toThrowError(/^capture requires an Image, but received/); - }); - }) - - describe("captureRegion", () => { - it("should capture the specified region of the screen and save image", async () => { - // GIVEN - const screenshot = new Image(100, 100, Buffer.from([]), 4, "test"); - 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(grabScreenMock).toHaveBeenCalledWith(regionToCapture) - expect(saveImageMock).toHaveBeenCalledWith(expectedData); - }); - - it("should throw in non-image input", async () => { - // GIVEN - 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)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - grabScreenRegion: grabScreenMock - })); - - const SUT = new ScreenClass(providerRegistryMock); - const imageName = "foobar.png" - - // WHEN - const result = SUT.captureRegion(imageName, regionToCapture) - - // THEN - expect(result).rejects.toThrowError(/^captureRegion requires an Image, but received/); - }); + it("should throw in non-image input", async () => { + // GIVEN + 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)); + providerRegistryMock.getScreen = jest.fn(() => + mockPartial({ + grabScreenRegion: grabScreenMock, + }) + ); + + const SUT = new ScreenClass(providerRegistryMock); + const imageName = "foobar.png"; + + // WHEN + const result = SUT.captureRegion(imageName, regionToCapture); + + // THEN + expect(result).rejects.toThrowError( + /^captureRegion requires an Image, but received/ + ); }); + }); }); diff --git a/lib/screen.class.ts b/lib/screen.class.ts index fe3b1ef4..5d571f89 100644 --- a/lib/screen.class.ts +++ b/lib/screen.class.ts @@ -1,378 +1,451 @@ -import {cwd} from "process"; -import {FileType} from "./file-type.enum"; -import {generateOutputPath} from "./generate-output-path.function"; -import {MatchRequest} from "./match-request.class"; -import {MatchResult} from "./match-result.class"; -import {isRegion, Region} from "./region.class"; -import {timeout} from "./util/timeout.function"; -import {Image, isImage} from "./image.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; -import {FirstArgumentType} from "./typings"; -import {isPoint, Point} from "./point.class"; -import {OptionalSearchParameters} from "./optionalsearchparameters.class"; +import { cwd } from "process"; +import { FileType } from "./file-type.enum"; +import { generateOutputPath } from "./generate-output-path.function"; +import { MatchRequest } from "./match-request.class"; +import { MatchResult } from "./match-result.class"; +import { isRegion, Region } from "./region.class"; +import { timeout } from "./util/timeout.function"; +import { Image, isImage } from "./image.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; +import { FirstArgumentType } from "./typings"; +import { isPoint, 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})`) - } + 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 ScreenClass} class provides methods to access screen content of a systems main display */ export class ScreenClass { - + /** + * Config object for {@link ScreenClass} class + */ + public config = { /** - * Config object for {@link ScreenClass} class + * Configures the required matching percentage for template images to be declared as a match */ - public config = { - /** - * Configures the required matching percentage for template images to be declared as a match - */ - confidence: 0.99, - - /** - * Configure whether to auto highlight all search results or not - */ - autoHighlight: false, - /** - * Configure highlighting duration - */ - highlightDurationMs: 500, - - /** - * Configure opacity of highlight window - */ - highlightOpacity: 0.25, - - /** - * Configures the path from which template images are loaded from - */ - resourceDirectory: cwd(), - }; + confidence: 0.99, /** - * {@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 + * Configure whether to auto highlight all search results or not */ - constructor( - private providerRegistry: ProviderRegistry, - private findHooks: Map = new Map()) { - } - - /** - * {@link width} returns the main screen width - * This refers to the hardware resolution. - * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher width in in actual pixels - */ - public width() { - return this.providerRegistry.getScreen().screenWidth(); - } - + autoHighlight: false, /** - * {@link height} returns the main screen height - * This refers to the hardware resolution. - * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher height in in actual pixels + * Configure highlighting duration */ - public height() { - return this.providerRegistry.getScreen().screenHeight(); - } + highlightDurationMs: 500, /** - * {@link find} will search for a single occurrence of a template image on a systems main screen - * @param template Template {@link Image} instance - * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence + * Configure opacity of highlight window */ - public async find( - template: Image | Promise, - params?: OptionalSearchParameters, - ): Promise { - const { - minMatch, - screenSize, - searchRegion, - screenImage, - searchMultipleScales - } = await this.getFindParameters(params); - - const needle = await ScreenClass.getNeedle(template); - - const matchRequest = new MatchRequest( - screenImage, - needle, - minMatch, - searchMultipleScales - ); - - return new Promise(async (resolve, reject) => { - try { - validateSearchRegion(searchRegion, screenSize); - const matchResult = await this.providerRegistry.getImageFinder().findMatch(matchRequest); - const possibleHooks = this.findHooks.get(needle) || []; - for (const hook of possibleHooks) { - await hook(matchResult); - } - const resultRegion = new Region( - searchRegion.left + matchResult.location.left, - searchRegion.top + matchResult.location.top, - matchResult.location.width, - matchResult.location.height - ) - if (this.config.autoHighlight) { - resolve(this.highlight(resultRegion)); - } else { - resolve(resultRegion); - } - } catch (e) { - reject( - `Searching for ${needle.id} failed. Reason: '${e}'`, - ); - } - }); - } + highlightOpacity: 0.25, /** - * {@link findAll} will search for every occurrences of a template image on a systems main screen - * @param template Template {@link Image} instance - * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence + * Configures the path from which template images are loaded from */ - public async findAll( - template: FirstArgumentType, - params?: OptionalSearchParameters, - ): Promise { - const { - minMatch, - screenSize, - searchRegion, - screenImage, - searchMultipleScales - } = await this.getFindParameters(params); - - const needle = await ScreenClass.getNeedle(template); - - const matchRequest = new MatchRequest( - screenImage, - needle, - minMatch, - searchMultipleScales + resourceDirectory: cwd(), + }; + + /** + * {@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 providerRegistry: ProviderRegistry, + private findHooks: Map = new Map< + Image, + FindHookCallback[] + >() + ) {} + + /** + * {@link width} returns the main screen width + * This refers to the hardware resolution. + * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher width in in actual pixels + */ + public width() { + return this.providerRegistry.getScreen().screenWidth(); + } + + /** + * {@link height} returns the main screen height + * This refers to the hardware resolution. + * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher height in in actual pixels + */ + public height() { + return this.providerRegistry.getScreen().screenHeight(); + } + + /** + * {@link find} will search for a single occurrence of a template image on a systems main screen + * @param template Template {@link Image} instance + * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence + */ + public async find( + template: Image | Promise, + params?: OptionalSearchParameters + ): Promise { + const { + minMatch, + screenSize, + searchRegion, + screenImage, + searchMultipleScales, + } = await this.getFindParameters(params); + + const needle = await ScreenClass.getNeedle(template); + + const matchRequest = new MatchRequest( + screenImage, + needle, + minMatch, + searchMultipleScales + ); + + return new Promise(async (resolve, reject) => { + try { + validateSearchRegion(searchRegion, screenSize); + const matchResult = await this.providerRegistry + .getImageFinder() + .findMatch(matchRequest); + const possibleHooks = this.findHooks.get(needle) || []; + for (const hook of possibleHooks) { + await hook(matchResult); + } + const resultRegion = new Region( + searchRegion.left + matchResult.location.left, + searchRegion.top + matchResult.location.top, + matchResult.location.width, + matchResult.location.height ); - - return new Promise(async (resolve, reject) => { - try { - validateSearchRegion(searchRegion, screenSize); - const matchResults = await this.providerRegistry.getImageFinder().findMatches(matchRequest); - const possibleHooks = this.findHooks.get(needle) || []; - for (const hook of possibleHooks) { - for (const matchResult of matchResults) { - await hook(matchResult); - } - } - const resultRegions = matchResults.map(matchResult => { - return new Region( - searchRegion.left + matchResult.location.left, - searchRegion.top + matchResult.location.top, - matchResult.location.width, - matchResult.location.height - ) - }) - if (this.config.autoHighlight) { - resultRegions.forEach(region => this.highlight(region)); - resolve(resultRegions); - } else { - resolve(resultRegions); - } - } catch (e) { - reject( - `Searching for ${needle.id} failed. Reason: '${e}'`, - ); - } - }); - } - - /** - * {@link highlight} highlights a screen {@link Region} for a certain duration by overlaying it with an opaque highlight window - * @param regionToHighlight The {@link Region} to highlight - */ - public async highlight(regionToHighlight: Region | Promise): Promise { - const highlightRegion = await regionToHighlight; - await this.providerRegistry.getScreen().highlightScreenRegion(highlightRegion, this.config.highlightDurationMs, this.config.highlightOpacity); - return highlightRegion; - } - - /** - * {@link waitFor} searches for a template image for a specified duration - * @param templateImage Filename of the template image, relative to {@link ScreenClass.config.resourceDirectory}, or an {@link Image} - * @param timeoutMs Timeout in milliseconds after which {@link waitFor} fails - * @param updateInterval Update interval in milliseconds to retry search - * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence - */ - public async waitFor( - templateImage: FirstArgumentType, - timeoutMs: number = 5000, - updateInterval: number = 500, - params?: OptionalSearchParameters, - ): Promise { - const needle = await templateImage; - - if (!isImage(needle)) { - throw Error(`waitFor requires an Image, but received ${JSON.stringify(templateImage)}`) + if (this.config.autoHighlight) { + resolve(this.highlight(resultRegion)); + } else { + resolve(resultRegion); } - return timeout(updateInterval, timeoutMs, () => this.find(needle, params), {signal: params?.abort}); - } - - /** - * {@link on} registers a callback which is triggered once a certain template image is found - * @param templateImage Template image to trigger the callback on - * @param callback The {@link FindHookCallback} function to trigger - */ - public on(templateImage: Image, callback: FindHookCallback) { - if (!isImage(templateImage)) { - throw Error(`on requires an Image, but received ${JSON.stringify(templateImage)}`) + } catch (e) { + reject(`Searching for ${needle.id} failed. Reason: '${e}'`); + } + }); + } + + /** + * {@link findAll} will search for every occurrences of a template image on a systems main screen + * @param template Template {@link Image} instance + * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence + */ + public async findAll( + template: FirstArgumentType, + params?: OptionalSearchParameters + ): Promise { + const { + minMatch, + screenSize, + searchRegion, + screenImage, + searchMultipleScales, + } = await this.getFindParameters(params); + + const needle = await ScreenClass.getNeedle(template); + + const matchRequest = new MatchRequest( + screenImage, + needle, + minMatch, + searchMultipleScales + ); + + return new Promise(async (resolve, reject) => { + try { + validateSearchRegion(searchRegion, screenSize); + const matchResults = await this.providerRegistry + .getImageFinder() + .findMatches(matchRequest); + const possibleHooks = this.findHooks.get(needle) || []; + for (const hook of possibleHooks) { + for (const matchResult of matchResults) { + await hook(matchResult); + } } - const existingHooks = this.findHooks.get(templateImage) || []; - this.findHooks.set(templateImage, [...existingHooks, callback]); - } - - /** - * {@link capture} captures a screenshot of a systems main display - * @param fileName Basename for the generated screenshot - * @param fileFormat The {@link FileType} for the generated screenshot - * @param filePath The output path for the generated screenshot (Default: {@link cwd}) - * @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty) - * @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty) - */ - public async capture( - fileName: string, - fileFormat: FileType = FileType.PNG, - filePath: string = cwd(), - fileNamePrefix: string = "", - fileNamePostfix: string = ""): Promise { - const currentScreen = await this.providerRegistry.getScreen().grabScreen(); - if (!isImage(currentScreen)) { - throw Error(`capture requires an Image, but received ${JSON.stringify(currentScreen)}`) + const resultRegions = matchResults.map((matchResult) => { + return new Region( + searchRegion.left + matchResult.location.left, + searchRegion.top + matchResult.location.top, + matchResult.location.width, + matchResult.location.height + ); + }); + if (this.config.autoHighlight) { + resultRegions.forEach((region) => this.highlight(region)); + resolve(resultRegions); + } else { + resolve(resultRegions); } - return this.saveImage( - currentScreen, - fileName, - fileFormat, - filePath, - fileNamePrefix, - fileNamePostfix); - } - - /** - * {@link grab} grabs screen content of a systems main display - */ - public async grab(): Promise { - return this.providerRegistry.getScreen().grabScreen(); + } catch (e) { + reject(`Searching for ${needle.id} failed. Reason: '${e}'`); + } + }); + } + + /** + * {@link highlight} highlights a screen {@link Region} for a certain duration by overlaying it with an opaque highlight window + * @param regionToHighlight The {@link Region} to highlight + */ + public async highlight( + regionToHighlight: Region | Promise + ): Promise { + const highlightRegion = await regionToHighlight; + await this.providerRegistry + .getScreen() + .highlightScreenRegion( + highlightRegion, + this.config.highlightDurationMs, + this.config.highlightOpacity + ); + return highlightRegion; + } + + /** + * {@link waitFor} searches for a template image for a specified duration + * @param templateImage Filename of the template image, relative to {@link ScreenClass.config.resourceDirectory}, or an {@link Image} + * @param timeoutMs Timeout in milliseconds after which {@link waitFor} fails + * @param updateInterval Update interval in milliseconds to retry search + * @param params {@link LocationParameters} which are used to fine tune search region and / or match confidence + */ + public async waitFor( + templateImage: FirstArgumentType, + timeoutMs: number = 5000, + updateInterval: number = 500, + params?: OptionalSearchParameters + ): Promise { + const needle = await templateImage; + + if (!isImage(needle)) { + throw Error( + `waitFor requires an Image, but received ${JSON.stringify( + templateImage + )}` + ); } - - /** - * {@link captureRegion} captures a screenshot of a region on the systems main display - * @param fileName Basename for the generated screenshot - * @param regionToCapture The region of the screen to capture in the screenshot - * @param fileFormat The {@link FileType} for the generated screenshot - * @param filePath The output path for the generated screenshot (Default: {@link cwd}) - * @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty) - * @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty) - */ - public async captureRegion( - fileName: string, - regionToCapture: Region | Promise, - fileFormat: FileType = FileType.PNG, - filePath: string = cwd(), - fileNamePrefix: string = "", - fileNamePostfix: string = ""): Promise { - const targetRegion = await regionToCapture; - if (!isRegion(targetRegion)) { - throw Error(`captureRegion requires an Region, but received ${JSON.stringify(targetRegion)}`) - } - const regionImage = await this.providerRegistry.getScreen().grabScreenRegion(targetRegion); - if (!isImage(regionImage)) { - throw Error(`captureRegion requires an Image, but received ${JSON.stringify(regionImage)}`) - } - return this.saveImage( - regionImage, - fileName, - fileFormat, - filePath, - fileNamePrefix, - fileNamePostfix); + return timeout(updateInterval, timeoutMs, () => this.find(needle, params), { + signal: params?.abort, + }); + } + + /** + * {@link on} registers a callback which is triggered once a certain template image is found + * @param templateImage Template image to trigger the callback on + * @param callback The {@link FindHookCallback} function to trigger + */ + public on(templateImage: Image, callback: FindHookCallback) { + if (!isImage(templateImage)) { + throw Error( + `on requires an Image, but received ${JSON.stringify(templateImage)}` + ); } - - /** - * {@link grabRegion} grabs screen content of a region on the systems main display - * @param regionToGrab The screen region to grab - */ - public async grabRegion(regionToGrab: Region | Promise): Promise { - return this.providerRegistry.getScreen().grabScreenRegion(await regionToGrab); + const existingHooks = this.findHooks.get(templateImage) || []; + this.findHooks.set(templateImage, [...existingHooks, callback]); + } + + /** + * {@link capture} captures a screenshot of a systems main display + * @param fileName Basename for the generated screenshot + * @param fileFormat The {@link FileType} for the generated screenshot + * @param filePath The output path for the generated screenshot (Default: {@link cwd}) + * @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty) + * @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty) + */ + public async capture( + fileName: string, + fileFormat: FileType = FileType.PNG, + filePath: string = cwd(), + fileNamePrefix: string = "", + fileNamePostfix: string = "" + ): Promise { + const currentScreen = await this.providerRegistry.getScreen().grabScreen(); + if (!isImage(currentScreen)) { + throw Error( + `capture requires an Image, but received ${JSON.stringify( + currentScreen + )}` + ); } - - /** - * {@link colorAt} returns RGBA color values for a certain pixel at {@link Point} p - * @param point Location to query color information from - */ - public async colorAt(point: Point | Promise) { - const screenContent = await this.providerRegistry.getScreen().grabScreen(); - const inputPoint = await point; - if (!isPoint(inputPoint)) { - throw Error(`colorAt requires a Point, but received ${JSON.stringify(inputPoint)}`) - } - const scaledPoint = new Point(inputPoint.x * screenContent.pixelDensity.scaleX, inputPoint.y * screenContent.pixelDensity.scaleY); - return this.providerRegistry.getImageProcessor().colorAt(screenContent, scaledPoint); + return this.saveImage( + currentScreen, + fileName, + fileFormat, + filePath, + fileNamePrefix, + fileNamePostfix + ); + } + + /** + * {@link grab} grabs screen content of a systems main display + */ + public async grab(): Promise { + return this.providerRegistry.getScreen().grabScreen(); + } + + /** + * {@link captureRegion} captures a screenshot of a region on the systems main display + * @param fileName Basename for the generated screenshot + * @param regionToCapture The region of the screen to capture in the screenshot + * @param fileFormat The {@link FileType} for the generated screenshot + * @param filePath The output path for the generated screenshot (Default: {@link cwd}) + * @param fileNamePrefix Filename prefix for the generated screenshot (Default: empty) + * @param fileNamePostfix Filename postfix for the generated screenshot (Default: empty) + */ + public async captureRegion( + fileName: string, + regionToCapture: Region | Promise, + fileFormat: FileType = FileType.PNG, + filePath: string = cwd(), + fileNamePrefix: string = "", + fileNamePostfix: string = "" + ): Promise { + const targetRegion = await regionToCapture; + if (!isRegion(targetRegion)) { + throw Error( + `captureRegion requires an Region, but received ${JSON.stringify( + targetRegion + )}` + ); } - - private async saveImage( - image: Image, - fileName: string, - fileFormat: FileType, - filePath: string, - fileNamePrefix: string, - fileNamePostfix: string) { - const outputPath = generateOutputPath(fileName, { - path: filePath, - postfix: fileNamePostfix, - prefix: fileNamePrefix, - type: fileFormat, - }); - await this.providerRegistry.getImageWriter().store({image, path: outputPath}) - return outputPath; + const regionImage = await this.providerRegistry + .getScreen() + .grabScreenRegion(targetRegion); + if (!isImage(regionImage)) { + throw Error( + `captureRegion requires an Image, but received ${JSON.stringify( + regionImage + )}` + ); } - - private async getFindParameters(params?: OptionalSearchParameters) { - const minMatch = params?.confidence ?? this.config.confidence; - const screenSize = await this.providerRegistry.getScreen().screenSize(); - const searchRegion = params?.searchRegion ?? screenSize; - const screenImage = await this.providerRegistry.getScreen().grabScreenRegion(searchRegion); - const searchMultipleScales = params?.searchMultipleScales ?? true; - - return ({ - minMatch, - screenSize, - searchRegion, - screenImage, - searchMultipleScales - }); + return this.saveImage( + regionImage, + fileName, + fileFormat, + filePath, + fileNamePrefix, + fileNamePostfix + ); + } + + /** + * {@link grabRegion} grabs screen content of a region on the systems main display + * @param regionToGrab The screen region to grab + */ + public async grabRegion( + regionToGrab: Region | Promise + ): Promise { + return this.providerRegistry + .getScreen() + .grabScreenRegion(await regionToGrab); + } + + /** + * {@link colorAt} returns RGBA color values for a certain pixel at {@link Point} p + * @param point Location to query color information from + */ + public async colorAt(point: Point | Promise) { + const screenContent = await this.providerRegistry.getScreen().grabScreen(); + const inputPoint = await point; + if (!isPoint(inputPoint)) { + throw Error( + `colorAt requires a Point, but received ${JSON.stringify(inputPoint)}` + ); } + const scaledPoint = new Point( + inputPoint.x * screenContent.pixelDensity.scaleX, + inputPoint.y * screenContent.pixelDensity.scaleY + ); + return this.providerRegistry + .getImageProcessor() + .colorAt(screenContent, scaledPoint); + } + + private async saveImage( + image: Image, + fileName: string, + fileFormat: FileType, + filePath: string, + fileNamePrefix: string, + fileNamePostfix: string + ) { + const outputPath = generateOutputPath(fileName, { + path: filePath, + postfix: fileNamePostfix, + prefix: fileNamePrefix, + type: fileFormat, + }); + await this.providerRegistry + .getImageWriter() + .store({ image, path: outputPath }); + return outputPath; + } + + private async getFindParameters(params?: OptionalSearchParameters) { + const minMatch = params?.confidence ?? this.config.confidence; + const screenSize = await this.providerRegistry.getScreen().screenSize(); + const searchRegion = params?.searchRegion ?? screenSize; + const screenImage = await this.providerRegistry + .getScreen() + .grabScreenRegion(searchRegion); + const searchMultipleScales = params?.searchMultipleScales ?? true; + + return { + minMatch, + screenSize, + searchRegion, + screenImage, + searchMultipleScales, + }; + } - private static async getNeedle(template: FirstArgumentType) { - const needle = await template; + private static async getNeedle( + template: FirstArgumentType + ) { + const needle = await template; - if (!isImage(needle)) { - throw Error(`find requires an Image, but received ${JSON.stringify(needle)}`) - } - return needle; + if (!isImage(needle)) { + throw Error( + `find requires an Image, but received ${JSON.stringify(needle)}` + ); } + return needle; + } } diff --git a/lib/screen.colorAt.spec.ts b/lib/screen.colorAt.spec.ts index a47abc6d..aaf63ab2 100644 --- a/lib/screen.colorAt.spec.ts +++ b/lib/screen.colorAt.spec.ts @@ -1,86 +1,112 @@ -import {Image, loadImage, Point, Region, RGBA, ScreenClass, ScreenProviderInterface} from "../index"; -import {mockPartial} from "sneer"; -import providerRegistry, {ProviderRegistry} from "./provider/provider-registry.class"; -import {ImageProcessor} from "./provider/image-processor.interface"; +import { + Image, + loadImage, + Point, + Region, + RGBA, + ScreenClass, + ScreenProviderInterface, +} from "../index"; +import { mockPartial } from "sneer"; +import providerRegistry, { + ProviderRegistry, +} from "./provider/provider-registry.class"; +import { ImageProcessor } from "./provider/image-processor.interface"; const searchRegion = new Region(0, 0, 1000, 1000); 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); - } - }) - }, - getImageProcessor(): ImageProcessor { - return providerRegistry.getImageProcessor(); - } + 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); + }, + }); + }, + getImageProcessor(): ImageProcessor { + return providerRegistry.getImageProcessor(); + }, }); describe("colorAt", () => { - it("should return the correct RGBA value for a given pixel", async () => { - // GIVEN - const screenshot = loadImage(`${__dirname}/../e2e/assets/checkers.png`); - const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - grabScreen: grabScreenMock - })); - providerRegistryMock.getImageProcessor() - const SUT = new ScreenClass(providerRegistryMock); - const expectedWhite = new RGBA(255, 255, 255, 255); - const expectedBlack = new RGBA(0, 0, 0, 255); + it("should return the correct RGBA value for a given pixel", async () => { + // GIVEN + const screenshot = loadImage(`${__dirname}/../e2e/assets/checkers.png`); + const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); + providerRegistryMock.getScreen = jest.fn(() => + mockPartial({ + grabScreen: grabScreenMock, + }) + ); + providerRegistryMock.getImageProcessor(); + const SUT = new ScreenClass(providerRegistryMock); + const expectedWhite = new RGBA(255, 255, 255, 255); + const expectedBlack = new RGBA(0, 0, 0, 255); - // WHEN - const white = await SUT.colorAt(new Point(64, 64)); - const black = await SUT.colorAt(new Point(192, 64)); + // WHEN + const white = await SUT.colorAt(new Point(64, 64)); + const black = await SUT.colorAt(new Point(192, 64)); - // THEN - expect(white).toStrictEqual(expectedWhite); - expect(black).toStrictEqual(expectedBlack); - }); + // THEN + expect(white).toStrictEqual(expectedWhite); + expect(black).toStrictEqual(expectedBlack); + }); - it("should account for pixel density when retrieving pixel color", async () => { - // GIVEN - const screenshot = await loadImage(`${__dirname}/../e2e/assets/checkers.png`); - screenshot.pixelDensity.scaleX = 2.0; - screenshot.pixelDensity.scaleY = 2.0; - const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - grabScreen: grabScreenMock - })); - providerRegistryMock.getImageProcessor() - const SUT = new ScreenClass(providerRegistryMock); - const expectedWhite = new RGBA(255, 255, 255, 255); - const expectedBlack = new RGBA(0, 0, 0, 255); + it("should account for pixel density when retrieving pixel color", async () => { + // GIVEN + const screenshot = await loadImage( + `${__dirname}/../e2e/assets/checkers.png` + ); + screenshot.pixelDensity.scaleX = 2.0; + screenshot.pixelDensity.scaleY = 2.0; + const grabScreenMock = jest.fn(() => Promise.resolve(screenshot)); + providerRegistryMock.getScreen = jest.fn(() => + mockPartial({ + grabScreen: grabScreenMock, + }) + ); + providerRegistryMock.getImageProcessor(); + const SUT = new ScreenClass(providerRegistryMock); + const expectedWhite = new RGBA(255, 255, 255, 255); + const expectedBlack = new RGBA(0, 0, 0, 255); - // WHEN - const white = await SUT.colorAt(new Point(32, 32)); - const black = await SUT.colorAt(new Point(96, 32)); + // WHEN + const white = await SUT.colorAt(new Point(32, 32)); + const black = await SUT.colorAt(new Point(96, 32)); - // THEN - expect(white).toStrictEqual(expectedWhite); - expect(black).toStrictEqual(expectedBlack); - }); + // THEN + expect(white).toStrictEqual(expectedWhite); + expect(black).toStrictEqual(expectedBlack); + }); - it("should throw on non-Point arguments", async () => { - // GIVEN - const grabScreenMock = jest.fn(() => - Promise.resolve( - new Image(10, 10, Buffer.from([]), 4, "test") - ) - ); - providerRegistryMock.getScreen = jest.fn(() => mockPartial({ - grabScreen: grabScreenMock - })); - const SUT = new ScreenClass(providerRegistryMock); + it("should throw on non-Point arguments", async () => { + // GIVEN + const grabScreenMock = jest.fn(() => + Promise.resolve(new Image(10, 10, Buffer.from([]), 4, "test")) + ); + providerRegistryMock.getScreen = jest.fn(() => + mockPartial({ + grabScreen: grabScreenMock, + }) + ); + const SUT = new ScreenClass(providerRegistryMock); - // WHEN - const result = SUT.colorAt({x: 10} as Point); + // WHEN + const result = SUT.colorAt({ x: 10 } as Point); - // THEN - await expect(result).rejects.toThrowError(/^colorAt requires a Point, but received/); - }); + // THEN + await expect(result).rejects.toThrowError( + /^colorAt requires a Point, but received/ + ); + }); }); diff --git a/lib/sleep.function.spec.ts b/lib/sleep.function.spec.ts index 1e00c9c0..629d9628 100644 --- a/lib/sleep.function.spec.ts +++ b/lib/sleep.function.spec.ts @@ -1,36 +1,36 @@ -import {busyWaitForNanoSeconds, sleep} from "./sleep.function"; +import { busyWaitForNanoSeconds, sleep } from "./sleep.function"; const maxTimeDeltaInMs = 3; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); describe("sleep", () => { - it("should resolve after x ms", async () => { - // GIVEN - const timeout = 500; - - // WHEN - const before = Date.now(); - await sleep(timeout); - const after = Date.now(); - - // THEN - expect(after - before).toBeGreaterThanOrEqual(timeout - maxTimeDeltaInMs); - }); + it("should resolve after x ms", async () => { + // GIVEN + const timeout = 500; + + // WHEN + const before = Date.now(); + await sleep(timeout); + const after = Date.now(); + + // THEN + expect(after - before).toBeGreaterThanOrEqual(timeout - maxTimeDeltaInMs); + }); }); describe("busyWaitForNanoSeconds", () => { - it("should resolve after x ns", async () => { - // GIVEN - const timeoutNs = 5_000_000; - const timeoutMs = 5; - - // WHEN - const before = Date.now(); - await busyWaitForNanoSeconds(timeoutNs); - const after = Date.now(); - - // THEN - expect(after - before).toBeGreaterThanOrEqual(timeoutMs - maxTimeDeltaInMs); - }); + it("should resolve after x ns", async () => { + // GIVEN + const timeoutNs = 5_000_000; + const timeoutMs = 5; + + // WHEN + const before = Date.now(); + await busyWaitForNanoSeconds(timeoutNs); + const after = Date.now(); + + // THEN + expect(after - before).toBeGreaterThanOrEqual(timeoutMs - maxTimeDeltaInMs); + }); }); diff --git a/lib/sleep.function.ts b/lib/sleep.function.ts index 74f9407f..b81056f3 100644 --- a/lib/sleep.function.ts +++ b/lib/sleep.function.ts @@ -1,13 +1,13 @@ export const sleep = async (ms: number) => { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); }; export const busyWaitForNanoSeconds = (duration: number) => { - return new Promise(res => { + return new Promise((res) => { const start = process.hrtime.bigint(); let isWaiting = true; while (isWaiting) { - if ((process.hrtime.bigint() - start) > duration) { + if (process.hrtime.bigint() - start > duration) { isWaiting = false; } } diff --git a/lib/typings.ts b/lib/typings.ts index 49f0e4d9..0b0dfadc 100644 --- a/lib/typings.ts +++ b/lib/typings.ts @@ -1 +1,6 @@ -export type FirstArgumentType = T extends (first: infer ArgType, ...args: any[]) => any ? ArgType : never; \ No newline at end of file +export type FirstArgumentType = T extends ( + first: infer ArgType, + ...args: any[] +) => any + ? ArgType + : never; diff --git a/lib/util/timeout.function.spec.ts b/lib/util/timeout.function.spec.ts index 9b3d861e..2f75a674 100644 --- a/lib/util/timeout.function.spec.ts +++ b/lib/util/timeout.function.spec.ts @@ -1,185 +1,187 @@ -import {timeout} from "./timeout.function"; +import { timeout } from "./timeout.function"; import AbortController from "node-abort-controller"; -import {sleep} from "../sleep.function"; +import { sleep } from "../sleep.function"; describe("timeout", () => { - it("should timeout after maxDuration if action rejects", async () => { - // GIVEN - const updateInterval = 200; - const maxDuration = 1000; - const action = jest.fn(() => { - return Promise.reject(false); - }); - - // WHEN - const start = Date.now(); - try { - await timeout(updateInterval, maxDuration, action); - } catch (e) { - expect(e).toBe(`Action timed out after ${maxDuration} ms`); - } - const end = Date.now(); - - // THEN - expect((end - start)).toBeGreaterThanOrEqual(maxDuration); + it("should timeout after maxDuration if action rejects", async () => { + // GIVEN + const updateInterval = 200; + const maxDuration = 1000; + const action = jest.fn(() => { + return Promise.reject(false); }); - it("should timeout after maxDuration if action resolve != true", async () => { - // GIVEN - const updateInterval = 200; - const maxDuration = 1000; - const action = jest.fn(async () => { - return false; - }); - - // WHEN - const start = Date.now(); - try { - await timeout(updateInterval, maxDuration, action); - } catch (e) { - expect(e).toEqual(`Action timed out after ${maxDuration} ms`); - } - const end = Date.now(); - - // THEN - expect((end - start)).toBeGreaterThanOrEqual(maxDuration); + // WHEN + const start = Date.now(); + try { + await timeout(updateInterval, maxDuration, action); + } catch (e) { + expect(e).toBe(`Action timed out after ${maxDuration} ms`); + } + const end = Date.now(); + + // THEN + expect(end - start).toBeGreaterThanOrEqual(maxDuration); + }); + + it("should timeout after maxDuration if action resolve != true", async () => { + // GIVEN + const updateInterval = 200; + const maxDuration = 1000; + const action = jest.fn(async () => { + return false; }); - it("should resolve after updateInterval if action resolves", async () => { - // GIVEN - const updateInterval = 200; - const maxDuration = 1000; - const action = jest.fn(() => { - return Promise.resolve(true); - }); - - // WHEN - const start = Date.now(); - await timeout(updateInterval, maxDuration, action); - const end = Date.now(); - - // THEN - expect((end - start)).toBeLessThan(updateInterval); - expect(action).toBeCalledTimes(1); + // WHEN + const start = Date.now(); + try { + await timeout(updateInterval, maxDuration, action); + } catch (e) { + expect(e).toEqual(`Action timed out after ${maxDuration} ms`); + } + const end = Date.now(); + + // THEN + expect(end - start).toBeGreaterThanOrEqual(maxDuration); + }); + + it("should resolve after updateInterval if action resolves", async () => { + // GIVEN + const updateInterval = 200; + const maxDuration = 1000; + const action = jest.fn(() => { + return Promise.resolve(true); }); - it("should resolve after updateInterval if action resolves != true", async () => { - // GIVEN - const updateInterval = 200; - const maxDuration = 1000; - const action = jest.fn(async () => { - return true; - }); - - // WHEN - const start = Date.now(); - await timeout(updateInterval, maxDuration, action); - const end = Date.now(); - - // THEN - expect((end - start)).toBeLessThan(updateInterval); - expect(action).toBeCalledTimes(1); + // WHEN + const start = Date.now(); + await timeout(updateInterval, maxDuration, action); + const end = Date.now(); + + // THEN + expect(end - start).toBeLessThan(updateInterval); + expect(action).toBeCalledTimes(1); + }); + + it("should resolve after updateInterval if action resolves != true", async () => { + // GIVEN + const updateInterval = 200; + const maxDuration = 1000; + const action = jest.fn(async () => { + return true; }); - it("should retry until action succeeds", async () => { - // GIVEN - const updateInterval = 200; - const maxDuration = 1000; - const delay = 2.5 * updateInterval; - const start = Date.now(); - const action = jest.fn(() => { - const interval = (Date.now() - start); - return new Promise((resolve, reject) => (interval > delay) ? resolve(true) : reject()); - }); - - // WHEN - const result = await timeout(updateInterval, maxDuration, action); - const end = Date.now(); - - // THEN - expect((end - start)).toBeGreaterThanOrEqual(delay); - expect(result).toBeTruthy(); + // WHEN + const start = Date.now(); + await timeout(updateInterval, maxDuration, action); + const end = Date.now(); + + // THEN + expect(end - start).toBeLessThan(updateInterval); + expect(action).toBeCalledTimes(1); + }); + + it("should retry until action succeeds", async () => { + // GIVEN + const updateInterval = 200; + const maxDuration = 1000; + const delay = 2.5 * updateInterval; + const start = Date.now(); + const action = jest.fn(() => { + const interval = Date.now() - start; + return new Promise((resolve, reject) => + interval > delay ? resolve(true) : reject() + ); }); - it("should fail after timeout if timeout < retry interval", async () => { - // GIVEN - const updateInterval = 1000; - const maxDuration = 200; - const action = jest.fn(() => Promise.resolve(false)); - - // WHEN - const start = Date.now(); - try { - await timeout(updateInterval, maxDuration, action); - } catch (e) { - expect(e).toEqual(`Action timed out after ${maxDuration} ms`); - } - const end = Date.now(); - - // THEN - expect(action).toBeCalledTimes(1); - expect((end - start)).toBeLessThan(updateInterval); + // WHEN + const result = await timeout(updateInterval, maxDuration, action); + const end = Date.now(); + + // THEN + expect(end - start).toBeGreaterThanOrEqual(delay); + expect(result).toBeTruthy(); + }); + + it("should fail after timeout if timeout < retry interval", async () => { + // GIVEN + const updateInterval = 1000; + const maxDuration = 200; + const action = jest.fn(() => Promise.resolve(false)); + + // WHEN + const start = Date.now(); + try { + await timeout(updateInterval, maxDuration, action); + } catch (e) { + expect(e).toEqual(`Action timed out after ${maxDuration} ms`); + } + const end = Date.now(); + + // THEN + expect(action).toBeCalledTimes(1); + expect(end - start).toBeLessThan(updateInterval); + }); + + it("should fail if action does not resolve within timeout", async () => { + // GIVEN + const updateInterval = 100; + const maxDuration = 200; + const action = jest.fn(() => { + return new Promise((_, reject) => { + setTimeout(() => reject(), 300); + }); }); - it("should fail if action does not resolve within timeout", async () => { - // GIVEN - const updateInterval = 100; - const maxDuration = 200; - const action = jest.fn(() => { - return new Promise((_, reject) => { - setTimeout(() => reject(), 300); - }) - }); - - // WHEN - const SUT = () => timeout(updateInterval, maxDuration, action); - - // THEN - await expect(SUT).rejects.toBe(`Action timed out after ${maxDuration} ms`); - expect(action).toBeCalledTimes(1); + // WHEN + const SUT = () => timeout(updateInterval, maxDuration, action); + + // THEN + await expect(SUT).rejects.toBe(`Action timed out after ${maxDuration} ms`); + expect(action).toBeCalledTimes(1); + }); + + it("should fail after timeout if no result is returned from long running action", async () => { + // GIVEN + const updateInterval = 100; + const maxDuration = 200; + const action = jest.fn(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(undefined as unknown as boolean); + }, 210); + }); }); - it("should fail after timeout if no result is returned from long running action", async () => { - // GIVEN - const updateInterval = 100; - const maxDuration = 200; - const action = jest.fn(() => { - return new Promise((resolve) => { - setTimeout(() => { - resolve((undefined as unknown) as boolean); - }, 210); - }); - }); - - // WHEN - const SUT = () => timeout(updateInterval, maxDuration, action); - - // THEN - await expect(SUT).rejects.toBe(`Action timed out after ${maxDuration} ms`); - expect(action).toBeCalledTimes(1); - await sleep(500); - expect(action).toBeCalledTimes(1); + // WHEN + const SUT = () => timeout(updateInterval, maxDuration, action); + + // THEN + await expect(SUT).rejects.toBe(`Action timed out after ${maxDuration} ms`); + expect(action).toBeCalledTimes(1); + await sleep(500); + expect(action).toBeCalledTimes(1); + }); + + it("should be externally abortable", async () => { + // GIVEN + const controller = new AbortController(); + const signal = controller.signal; + const updateInterval = 100; + const maxDuration = 3000; + const action = jest.fn(() => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(undefined as unknown as boolean); + }, 20); + }); }); - it("should be externally abortable", async () => { - // GIVEN - const controller = new AbortController(); - const signal = controller.signal; - const updateInterval = 100; - const maxDuration = 3000; - const action = jest.fn(() => { - return new Promise((_, reject) => { - setTimeout(() => { - reject((undefined as unknown) as boolean); - }, 20); - }); - }); - - // WHEN - const SUT = timeout(updateInterval, maxDuration, action, {signal}); - setTimeout(() => controller.abort(), 1000); - - // THEN - await expect(SUT).rejects.toBe(`Action aborted by signal`); - }); + // WHEN + const SUT = timeout(updateInterval, maxDuration, action, { signal }); + setTimeout(() => controller.abort(), 1000); + + // THEN + await expect(SUT).rejects.toBe(`Action aborted by signal`); + }); }); diff --git a/lib/util/timeout.function.ts b/lib/util/timeout.function.ts index ed656256..68222fa4 100644 --- a/lib/util/timeout.function.ts +++ b/lib/util/timeout.function.ts @@ -1,58 +1,60 @@ -import {AbortSignal} from "node-abort-controller"; +import { AbortSignal } from "node-abort-controller"; export interface TimoutConfig { - signal?: AbortSignal + signal?: AbortSignal; } -export function timeout(updateIntervalMs: number, maxDurationMs: number, action: (...params: any) => Promise, config?: TimoutConfig): Promise { - return new Promise((resolve, reject) => { - let interval: NodeJS.Timeout; - let timerCleaned = false - - if (config?.signal) { - config.signal.onabort = () => { - cleanupTimer(); - reject(`Action aborted by signal`); - } - } - - function executeInterval() { - action().then(validateResult).catch(handleRejection); - } - - function validateResult(result: R) { - if (!result && !timerCleaned) { - interval = setTimeout(executeInterval, updateIntervalMs); - } else { - cleanupTimer(); - resolve(result); - } - } - - function handleRejection() { - if (!timerCleaned) { - interval = setTimeout(executeInterval, updateIntervalMs); - } - } - - function cleanupTimer() { - timerCleaned = true - if (maxTimeout) { - clearTimeout(maxTimeout); - } - if (interval) { - clearTimeout(interval); - } - } - - const maxTimeout = setTimeout( - () => { - cleanupTimer(); - reject(`Action timed out after ${maxDurationMs} ms`); - }, - maxDurationMs - ); - - executeInterval() - }); +export function timeout( + updateIntervalMs: number, + maxDurationMs: number, + action: (...params: any) => Promise, + config?: TimoutConfig +): Promise { + return new Promise((resolve, reject) => { + let interval: NodeJS.Timeout; + let timerCleaned = false; + + if (config?.signal) { + config.signal.onabort = () => { + cleanupTimer(); + reject(`Action aborted by signal`); + }; + } + + function executeInterval() { + action().then(validateResult).catch(handleRejection); + } + + function validateResult(result: R) { + if (!result && !timerCleaned) { + interval = setTimeout(executeInterval, updateIntervalMs); + } else { + cleanupTimer(); + resolve(result); + } + } + + function handleRejection() { + if (!timerCleaned) { + interval = setTimeout(executeInterval, updateIntervalMs); + } + } + + function cleanupTimer() { + timerCleaned = true; + if (maxTimeout) { + clearTimeout(maxTimeout); + } + if (interval) { + clearTimeout(interval); + } + } + + const maxTimeout = setTimeout(() => { + cleanupTimer(); + reject(`Action timed out after ${maxDurationMs} ms`); + }, maxDurationMs); + + executeInterval(); + }); } diff --git a/lib/window.class.spec.ts b/lib/window.class.spec.ts index b7cbb42d..f9121780 100644 --- a/lib/window.class.spec.ts +++ b/lib/window.class.spec.ts @@ -1,51 +1,50 @@ -import {Window} from "./window.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; -import {mockPartial} from "sneer"; -import {WindowProviderInterface} from "./provider"; +import { Window } from "./window.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; +import { mockPartial } from "sneer"; +import { WindowProviderInterface } from "./provider"; -jest.mock('jimp', () => { -}); +jest.mock("jimp", () => {}); describe("Window class", () => { - it("should retrieve the window region via provider", async () => { - // GIVEN - const windowMock = jest.fn(); - const providerRegistryMock = mockPartial({ - getWindow(): WindowProviderInterface { - return mockPartial({ - getWindowRegion: windowMock - }) - } - }) - const mockWindowHandle = 123; - const SUT = new Window(providerRegistryMock, mockWindowHandle); - - // WHEN - await SUT.region - - // THEN - expect(windowMock).toBeCalledTimes(1); - expect(windowMock).toBeCalledWith(mockWindowHandle); + it("should retrieve the window region via provider", async () => { + // GIVEN + const windowMock = jest.fn(); + const providerRegistryMock = mockPartial({ + getWindow(): WindowProviderInterface { + return mockPartial({ + getWindowRegion: windowMock, + }); + }, }); + const mockWindowHandle = 123; + const SUT = new Window(providerRegistryMock, mockWindowHandle); - it("should retrieve the window title via provider", async () => { - // GIVEN - const windowMock = jest.fn(); - const providerRegistryMock = mockPartial({ - getWindow(): WindowProviderInterface { - return mockPartial({ - getWindowTitle: windowMock - }) - } - }) - const mockWindowHandle = 123; - const SUT = new Window(providerRegistryMock, mockWindowHandle); + // WHEN + await SUT.region; - // WHEN - await SUT.title + // THEN + expect(windowMock).toBeCalledTimes(1); + expect(windowMock).toBeCalledWith(mockWindowHandle); + }); - // THEN - expect(windowMock).toBeCalledTimes(1); - expect(windowMock).toBeCalledWith(mockWindowHandle); + it("should retrieve the window title via provider", async () => { + // GIVEN + const windowMock = jest.fn(); + const providerRegistryMock = mockPartial({ + getWindow(): WindowProviderInterface { + return mockPartial({ + getWindowTitle: windowMock, + }); + }, }); -}); \ No newline at end of file + const mockWindowHandle = 123; + const SUT = new Window(providerRegistryMock, mockWindowHandle); + + // WHEN + await SUT.title; + + // THEN + expect(windowMock).toBeCalledTimes(1); + expect(windowMock).toBeCalledWith(mockWindowHandle); + }); +}); diff --git a/lib/window.class.ts b/lib/window.class.ts index 834b6583..d139b876 100644 --- a/lib/window.class.ts +++ b/lib/window.class.ts @@ -1,15 +1,17 @@ -import {Region} from "./region.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; +import { Region } from "./region.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; export class Window { - constructor(private providerRegistry: ProviderRegistry, private windowHandle: number) { - } + constructor( + private providerRegistry: ProviderRegistry, + private windowHandle: number + ) {} - get title(): Promise { - return this.providerRegistry.getWindow().getWindowTitle(this.windowHandle); - } + get title(): Promise { + return this.providerRegistry.getWindow().getWindowTitle(this.windowHandle); + } - get region(): Promise { - return this.providerRegistry.getWindow().getWindowRegion(this.windowHandle); - } -} \ No newline at end of file + get region(): Promise { + return this.providerRegistry.getWindow().getWindowRegion(this.windowHandle); + } +} diff --git a/lib/window.function.spec.ts b/lib/window.function.spec.ts index e4df42f9..9ddce1f4 100644 --- a/lib/window.function.spec.ts +++ b/lib/window.function.spec.ts @@ -1,35 +1,35 @@ -import {createWindowApi} from "./window.function"; -import {Window} from "./window.class"; +import { createWindowApi } from "./window.function"; +import { Window } from "./window.class"; import providerRegistry from "./provider/provider-registry.class"; -jest.mock('jimp', () => {}); +jest.mock("jimp", () => {}); describe("WindowApi", () => { - describe("getWindows", () => { - it("should return a list of open Windows", async () => { - // GIVEN - const SUT = createWindowApi(providerRegistry); + describe("getWindows", () => { + it("should return a list of open Windows", async () => { + // GIVEN + const SUT = createWindowApi(providerRegistry); - // WHEN - const windows = await SUT.getWindows() + // WHEN + const windows = await SUT.getWindows(); - // THEN - windows.forEach(wnd => { - expect(wnd).toEqual(expect.any(Window)); - }); - }); + // THEN + windows.forEach((wnd) => { + expect(wnd).toEqual(expect.any(Window)); + }); }); + }); - describe("getActiveWindow", () => { - it("should return the a single Window which is currently active", async () => { - // GIVEN - const SUT = createWindowApi(providerRegistry); + describe("getActiveWindow", () => { + it("should return the a single Window which is currently active", async () => { + // GIVEN + const SUT = createWindowApi(providerRegistry); - // WHEN - const window = await SUT.getActiveWindow(); + // WHEN + const window = await SUT.getActiveWindow(); - // THEN - expect(window).toEqual(expect.any(Window)); - }); + // THEN + expect(window).toEqual(expect.any(Window)); }); -}); \ No newline at end of file + }); +}); diff --git a/lib/window.function.ts b/lib/window.function.ts index 2aba056a..133872c8 100644 --- a/lib/window.function.ts +++ b/lib/window.function.ts @@ -1,9 +1,11 @@ import { WindowApi } from "./window-api.interface"; import { Window } from "./window.class"; -import {ProviderRegistry} from "./provider/provider-registry.class"; +import { ProviderRegistry } from "./provider/provider-registry.class"; -export const createWindowApi = (providerRegistry: ProviderRegistry): WindowApi => { - return ({ +export const createWindowApi = ( + providerRegistry: ProviderRegistry +): WindowApi => { + return { async getActiveWindow(): Promise { const windowHandle = await providerRegistry.getWindow().getActiveWindow(); return new Window(providerRegistry, windowHandle); @@ -14,5 +16,5 @@ export const createWindowApi = (providerRegistry: ProviderRegistry): WindowApi = return new Window(providerRegistry, handle); }); }, - }); -}; \ No newline at end of file + }; +}; diff --git a/package-lock.json b/package-lock.json index 4a6adcfe..f26c7cb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,12 @@ "devDependencies": { "@types/jest": "27.0.1", "@types/node": "16.7.10", + "husky": "^8.0.1", "istanbul-merge": "1.1.1", "jest": "27.1.0", + "lint-staged": "^13.0.3", "nyc": "15.1.0", + "prettier": "2.7.1", "rimraf": "3.0.2", "sneer": "1.0.1", "ts-jest": "27.0.5", @@ -2431,6 +2434,15 @@ "node": ">=0.8" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2874,6 +2886,72 @@ "node": ">=8" } }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -3096,6 +3174,15 @@ "node": ">=4.0.0" } }, + "node_modules/commander": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3192,9 +3279,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -3384,6 +3471,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4216,6 +4309,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6295,11 +6403,391 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lint-staged": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.17", + "commander": "^9.3.0", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.5", + "listr2": "^4.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/lint-staged/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lint-staged/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lint-staged/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" }, + "node_modules/listr2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.5", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -6421,6 +6909,99 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6520,13 +7101,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -6793,6 +7374,15 @@ "node": "*" } }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -7027,9 +7617,9 @@ "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -7038,6 +7628,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -7120,6 +7722,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.1.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.1.0.tgz", @@ -7406,6 +8023,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -7542,9 +8165,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/sisteransi": { "version": "1.0.5", @@ -7561,6 +8184,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sneer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sneer/-/sneer-1.0.1.tgz", @@ -8676,6 +9339,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "node_modules/yaml": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", + "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", @@ -10633,6 +11305,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10962,6 +11640,50 @@ "restore-cursor": "^3.1.0" } }, + "cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, "cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -11145,6 +11867,12 @@ "typical": "^4.0.0" } }, + "commander": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -11231,9 +11959,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -11383,6 +12111,12 @@ } } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -12013,6 +12747,12 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -13614,11 +14354,272 @@ "type-check": "~0.3.2" } }, + "lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true + }, + "lint-staged": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", + "dev": true, + "requires": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.17", + "commander": "^9.3.0", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.5", + "listr2": "^4.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.1" + }, + "dependencies": { + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" }, + "listr2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "dev": true, + "requires": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.5", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -13724,6 +14725,74 @@ "is-unicode-supported": "^0.1.0" } }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -13806,13 +14875,13 @@ "dev": true }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" } }, "mime": { @@ -14031,6 +15100,12 @@ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, "omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -14211,9 +15286,15 @@ "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true }, "pify": { @@ -14274,6 +15355,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, "pretty-format": { "version": "27.1.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.1.0.tgz", @@ -14491,6 +15578,12 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -14589,9 +15682,9 @@ } }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "sisteransi": { "version": "1.0.5", @@ -14605,6 +15698,30 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + } + } + }, "sneer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sneer/-/sneer-1.0.1.tgz", @@ -15457,6 +16574,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yaml": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", + "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", + "dev": true + }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/package.json b/package.json index 765ed5f1..6c4cad81 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "publish-next": "npm publish --tag next", "prepublishOnly": "npm run compile", "versionBump": "bump --tag --push --all", - "typedoc": "typedoc --options ./typedoc.js --entryPointStrategy expand ./lib" + "typedoc": "typedoc --options ./typedoc.js --entryPointStrategy expand ./lib", + "prepare": "husky install" }, "dependencies": { "@nut-tree/libnut": "2.3.0", @@ -68,9 +69,12 @@ "devDependencies": { "@types/jest": "27.0.1", "@types/node": "16.7.10", + "husky": "8.0.1", "istanbul-merge": "1.1.1", "jest": "27.1.0", + "lint-staged": "13.0.3", "nyc": "15.1.0", + "prettier": "2.7.1", "rimraf": "3.0.2", "sneer": "1.0.1", "ts-jest": "27.0.5", @@ -78,5 +82,8 @@ "typedoc": "0.23.14", "typescript": "4.8.3", "version-bump-prompt": "6.1.0" + }, + "lint-staged": { + "**/*": "prettier --write --ignore-unknown" } } diff --git a/tsconfig.json b/tsconfig.json index 293a177c..7cfd6840 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,7 @@ "compilerOptions": { "target": "ES2018", "module": "commonjs", - "lib": [ - "es6" - ], + "lib": ["es6"], "outDir": "./dist", "declaration": true, "declarationMap": true, @@ -15,13 +13,8 @@ "noUnusedParameters": true, "noImplicitReturns": true, - "esModuleInterop": true, + "esModuleInterop": true }, - "include": [ - "lib/**/*.ts", - "index.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["lib/**/*.ts", "index.ts"], + "exclude": ["node_modules"] } diff --git a/tslint.json b/tslint.json index 209426b0..4d33018b 100644 --- a/tslint.json +++ b/tslint.json @@ -1,8 +1,6 @@ { "defaultSeverity": "error", - "extends": [ - "tslint:recommended" - ], + "extends": ["tslint:recommended"], "jsRules": {}, "rules": { "no-empty": false, diff --git a/typedoc.js b/typedoc.js index 3cdeb11c..e3e98cde 100644 --- a/typedoc.js +++ b/typedoc.js @@ -1,14 +1,14 @@ module.exports = { exclude: [ - '**/dist/**', - '**/node_modules/**', - '**/*.spec.ts', - '**/__mocks__/**', + "**/dist/**", + "**/node_modules/**", + "**/*.spec.ts", + "**/__mocks__/**", ], - readme: 'README.md', + readme: "README.md", excludePrivate: true, excludeExternals: true, excludeProtected: true, hideGenerator: true, - theme: 'default', + theme: "default", };