diff --git a/index.ts b/index.ts index f7b26a83..d47b62f2 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,3 @@ -import {NativeAdapter} from "./lib/adapter/native.adapter.class"; -import {VisionAdapter} from "./lib/adapter/vision.adapter.class"; import {AssertClass} from "./lib/assert.class"; import {ClipboardClass} from "./lib/clipboard.class"; import {KeyboardClass} from "./lib/keyboard.class"; @@ -37,18 +35,16 @@ export {Region} from "./lib/region.class"; export {Window} from "./lib/window.class"; export {FileType} from "./lib/file-type.enum"; -const screenActions = new VisionAdapter(providerRegistry); -const nativeActions = new NativeAdapter(providerRegistry); const lineHelper = new LineHelper(); -const clipboard = new ClipboardClass(nativeActions); -const keyboard = new KeyboardClass(nativeActions); -const mouse = new MouseClass(nativeActions); -const screen = new ScreenClass(screenActions); +const clipboard = new ClipboardClass(providerRegistry); +const keyboard = new KeyboardClass(providerRegistry); +const mouse = new MouseClass(providerRegistry); +const screen = new ScreenClass(providerRegistry); const assert = new AssertClass(screen); -const {straightTo, up, down, left, right} = createMovementApi(nativeActions, lineHelper); -const {getWindows, getActiveWindow} = createWindowApi(nativeActions); +const {straightTo, up, down, left, right} = createMovementApi(providerRegistry, lineHelper); +const {getWindows, getActiveWindow} = createWindowApi(providerRegistry); const loadImage = providerRegistry.getImageReader().load; const saveImage = providerRegistry.getImageWriter().store; diff --git a/lib/adapter/native.adapter.class.spec.ts b/lib/adapter/native.adapter.class.spec.ts deleted file mode 100644 index 38543416..00000000 --- a/lib/adapter/native.adapter.class.spec.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Button } from "../button.enum"; -import { Key } from "../key.enum"; -import { Point } from "../point.class"; -import { NativeAdapter } from "./native.adapter.class"; -import ClipboardAction from "../provider/native/clipboardy-clipboard.class"; -import KeyboardAction from "../provider/native/libnut-keyboard.class"; -import MouseAction from "../provider/native/libnut-mouse.class"; -import WindowAction from "../provider/native/libnut-window.class"; -import providerRegistry from "../provider/provider-registry.class"; - -jest.mock("../provider/native/clipboardy-clipboard.class"); -jest.mock("../provider/native/libnut-mouse.class"); -jest.mock("../provider/native/libnut-keyboard.class"); -jest.mock("../provider/native/libnut-window.class"); -jest.mock('jimp', () => {}); - -let clipboardMock: ClipboardAction; -let keyboardMock: KeyboardAction; -let mouseMock: MouseAction; -let windowMock: WindowAction; - -beforeAll(() => { - clipboardMock = new ClipboardAction(); - keyboardMock = new KeyboardAction(); - mouseMock = new MouseAction(); - windowMock = new WindowAction(); - providerRegistry.registerClipboardProvider(clipboardMock); - providerRegistry.registerKeyboardProvider(keyboardMock); - providerRegistry.registerMouseProvider(mouseMock); - providerRegistry.registerWindowProvider(windowMock); -}); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe("NativeAdapter class", () => { - describe("MouseAction", () => { - it("should delegate calls to setMouseDelay", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const delay = 5; - - // WHEN - SUT.setMouseDelay(delay); - - // THEN - expect(mouseMock.setMouseDelay).toBeCalledTimes(1); - expect(mouseMock.setMouseDelay).toBeCalledWith(delay); - }); - - it("should delegate calls to setMousePosition", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const newPosition = new Point(10, 10); - - // WHEN - await SUT.setMousePosition(newPosition); - - // THEN - expect(mouseMock.setMousePosition).toBeCalledTimes(1); - expect(mouseMock.setMousePosition).toBeCalledWith(newPosition); - }); - - it("should delegate calls to currentMousePosition", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.currentMousePosition(); - - // THEN - expect(mouseMock.currentMousePosition).toBeCalledTimes(1); - }); - - it("should delegate calls to leftClick", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.leftClick(); - - // THEN - expect(mouseMock.leftClick).toBeCalledTimes(1); - }); - - it("should delegate calls to rightClick", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.rightClick(); - - // THEN - expect(mouseMock.rightClick).toBeCalledTimes(1); - }); - - it("should delegate calls to middleClick", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.middleClick(); - - // THEN - expect(mouseMock.middleClick).toBeCalledTimes(1); - }); - - it("should delegate calls to pressButton", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const buttonToPress = Button.LEFT; - - // WHEN - await SUT.pressButton(buttonToPress); - - // THEN - expect(mouseMock.pressButton).toBeCalledTimes(1); - expect(mouseMock.pressButton).toBeCalledWith(buttonToPress); - }); - - it("should delegate calls to releaseButton", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const buttonToRelease = Button.LEFT; - - // WHEN - await SUT.releaseButton(buttonToRelease); - - // THEN - expect(mouseMock.releaseButton).toBeCalledTimes(1); - expect(mouseMock.releaseButton).toBeCalledWith(buttonToRelease); - }); - }); - - describe("KeyboardAction", () => { - it("should delegate calls to pressKey", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const keyToPress = Key.A; - - // WHEN - await SUT.pressKey(keyToPress); - - // THEN - expect(keyboardMock.pressKey).toBeCalledTimes(1); - expect(keyboardMock.pressKey).toBeCalledWith(keyToPress); - }); - - it("should delegate calls to releaseButton", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const keyToRelease = Key.A; - - // WHEN - await SUT.releaseKey(keyToRelease); - - // THEN - expect(keyboardMock.releaseKey).toBeCalledTimes(1); - expect(keyboardMock.releaseKey).toBeCalledWith(keyToRelease); - }); - - it("should delegate calls to click", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const keyToClick = Key.A; - - // WHEN - await SUT.click(keyToClick); - - // THEN - expect(keyboardMock.click).toBeCalledTimes(1); - expect(keyboardMock.click).toBeCalledWith(keyToClick); - }); - - it("should delegate calls to type", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const stringToType = "testString"; - - // WHEN - await SUT.type(stringToType); - - // THEN - expect(keyboardMock.type).toBeCalledTimes(1); - expect(keyboardMock.type).toBeCalledWith(stringToType); - }); - }); - - describe("ClipboardAction", () => { - it("should delegate calls to copy", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const stringToCopy = "testString"; - - // WHEN - await SUT.copy(stringToCopy); - - // THEN - expect(clipboardMock.copy).toBeCalledTimes(1); - expect(clipboardMock.copy).toBeCalledWith(stringToCopy); - }); - - it("should delegate calls to paste", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.paste(); - - // THEN - expect(clipboardMock.paste).toBeCalledTimes(1); - }); - }); - - describe("WindowAction", () => { - it("should delegate calls to getActiveWindow", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.getActiveWindow(); - - // THEN - expect(windowMock.getActiveWindow).toBeCalledTimes(1); - }); - - it("should delegate calls to getWindows", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - - // WHEN - await SUT.getWindows(); - - // THEN - expect(windowMock.getWindows).toBeCalledTimes(1); - }); - - it("should delegate calls to getWindowTitle", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const windowHandle = 123; - - // WHEN - await SUT.getWindowTitle(windowHandle); - - // THEN - expect(windowMock.getWindowTitle).toBeCalledTimes(1); - }); - - it("should delegate calls to getWindowRegion", async () => { - // GIVEN - const SUT = new NativeAdapter(providerRegistry); - const windowHandle = 123; - - // WHEN - await SUT.getWindowRegion(windowHandle); - - // THEN - expect(windowMock.getWindowRegion).toBeCalledTimes(1); - }); - }); -}); diff --git a/lib/adapter/native.adapter.class.ts b/lib/adapter/native.adapter.class.ts deleted file mode 100644 index 264b39e4..00000000 --- a/lib/adapter/native.adapter.class.ts +++ /dev/null @@ -1,219 +0,0 @@ -import {Button} from "../button.enum"; -import {Key} from "../key.enum"; -import {Point} from "../point.class"; -import {Region} from "../region.class"; -import {ProviderRegistry} from "../provider/provider-registry.class"; - -/** - * {@link NativeAdapter} serves as an abstraction layer for all OS level interactions. - * - * This allows to provide a high level interface for native actions, - * without having to spread (possibly) multiple dependencies all over the code. - * All actions which involve the OS are bundled in this adapter. - */ -export class NativeAdapter { - /** - * {@link NativeAdapter} class constructor - * @param registry: {@link ProviderRegistry} to access providers - */ - constructor( - private registry: ProviderRegistry - ) { - } - - /** - * {@link setMouseDelay} configures mouse speed for movement - * - * @param delay Mouse delay in milliseconds - */ - public setMouseDelay(delay: number): void { - this.registry.getMouse().setMouseDelay(delay); - } - - /** - * {@link setKeyboardDelay} configures keyboard delay between key presses - * - * @param delay The keyboard delay in milliseconds - */ - public setKeyboardDelay(delay: number): void { - this.registry.getKeyboard().setKeyboardDelay(delay); - } - - /** - * {@link setMousePosition} changes the current mouse cursor position to a given {@link Point} - * - * @param p The new cursor position at {@link Point} p - */ - public setMousePosition(p: Point): Promise { - return this.registry.getMouse().setMousePosition(p); - } - - /** - * {@link currentMousePosition} returns the current mouse position - * - * @returns Current cursor position at a certain {@link Point} - */ - public currentMousePosition(): Promise { - return this.registry.getMouse().currentMousePosition(); - } - - /** - * {@link leftClick} triggers a native left-click event via OS API - */ - public leftClick(): Promise { - return this.registry.getMouse().leftClick(); - } - - /** - * {@link rightClick} triggers a native right-click event via OS API - */ - public rightClick(): Promise { - return this.registry.getMouse().rightClick(); - } - - /** - * {@link middleClick} triggers a native middle-click event via OS API - */ - public middleClick(): Promise { - return this.registry.getMouse().middleClick(); - } - - /** - * {@link pressButton} presses and holds a mouse {@link Button} - * - * @param btn The mouse {@link Button} to press - */ - public pressButton(btn: Button): Promise { - return this.registry.getMouse().pressButton(btn); - } - - /** - * {@link releaseButton} releases a mouse {@link Button} previously clicked via {@link pressButton} - * - * @param btn The mouse {@link Button} to release - */ - public releaseButton(btn: Button): Promise { - return this.registry.getMouse().releaseButton(btn); - } - - /** - * {@link type} types a given string via native keyboard events - * - * @param input The text to type - */ - public type(input: string): Promise { - return this.registry.getKeyboard().type(input); - } - - /** - * {@link click} clicks a {@link Key} via native keyboard event - * - * @param keys Array of {@link Key}s to click - */ - public click(...keys: Key[]): Promise { - return this.registry.getKeyboard().click(...keys); - } - - /** - * {@link pressKey} presses and holds a given {@link Key} - * - * @param keys Array of {@link Key}s to press and hold - */ - public pressKey(...keys: Key[]): Promise { - return this.registry.getKeyboard().pressKey(...keys); - } - - /** - * {@link releaseKey} releases a {@link Key} previously presses via {@link pressKey} - * - * @param keys Array of {@link Key}s to release - */ - public releaseKey(...keys: Key[]): Promise { - return this.registry.getKeyboard().releaseKey(...keys); - } - - /** - * {@link scrollUp} triggers an upwards mouse wheel scroll - * - * @param amount The amount of 'ticks' to scroll - */ - public scrollUp(amount: number): Promise { - return this.registry.getMouse().scrollUp(amount); - } - - /** - * {@link scrollDown} triggers a downward mouse wheel scroll - * - * @param amount The amount of 'ticks' to scroll - */ - public scrollDown(amount: number): Promise { - return this.registry.getMouse().scrollDown(amount); - } - - /** - * {@link scrollLeft} triggers a left mouse scroll - * - * @param amount The amount of 'ticks' to scroll - */ - public scrollLeft(amount: number): Promise { - return this.registry.getMouse().scrollLeft(amount); - } - - /** - * {@link scrollRight} triggers a right mouse scroll - * - * @param amount The amount of 'ticks' to scroll - */ - public scrollRight(amount: number): Promise { - return this.registry.getMouse().scrollRight(amount); - } - - /** - * {@link copy} copies a given text to the system clipboard - * - * @param text The text to copy - */ - public copy(text: string): Promise { - return this.registry.getClipboard().copy(text); - } - - /** - * {@link paste} pastes the current text on the system clipboard - * - * @returns The clipboard text - */ - public paste(): Promise { - return this.registry.getClipboard().paste(); - } - - public getWindows(): Promise { - return this.registry.getWindow().getWindows(); - } - - /** - * {@link getActiveWindow} returns the window handle of the currently active foreground window - * - * @returns The handle to the currently active foreground window - */ - public getActiveWindow(): Promise { - return this.registry.getWindow().getActiveWindow(); - } - - /** - * {@link getWindowTitle} returns the title of a window addressed via its window handle - * - * @returns A string representing the title of a window addressed via its window handle - */ - public getWindowTitle(windowHandle: number): Promise { - return this.registry.getWindow().getWindowTitle(windowHandle); - } - - /** - * {@link getWindowRegion} returns a {@link Region} object representing the size and position of the window addressed via its window handle - * - * @returns The {@link Region} occupied by the window addressed via its window handle - */ - public getWindowRegion(windowHandle: number): Promise { - return this.registry.getWindow().getWindowRegion(windowHandle); - } -} diff --git a/lib/adapter/vision.adapter.class.spec.ts b/lib/adapter/vision.adapter.class.spec.ts deleted file mode 100644 index 0dfc58da..00000000 --- a/lib/adapter/vision.adapter.class.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import {Image} from "../image.class"; -import {MatchRequest} from "../match-request.class"; -import ScreenAction from "../provider/native/libnut-screen.class"; -import {Region} from "../region.class"; -import {VisionAdapter} from "./vision.adapter.class"; -import providerRegistry from "../provider/provider-registry.class"; -import {MatchResult} from "../match-result.class"; - -jest.mock('jimp', () => {}); -jest.mock("../provider/native/libnut-screen.class"); - -const finderMock = { - findMatch: jest.fn(() => Promise.resolve( - new MatchResult(0, - new Region(0, 0, 100, 100) - ) - )), - findMatches: jest.fn(() => Promise.resolve([])) -} - -describe("VisionAdapter class", () => { - it("should delegate calls to grabScreen", () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - - // WHEN - SUT.grabScreen(); - - // THEN - expect(screenMock.grabScreen).toBeCalledTimes(1); - }); - - it("should delegate calls to grabScreenRegion", async () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - const screenRegion = new Region(0, 0, 100, 100); - - // WHEN - await SUT.grabScreenRegion(screenRegion); - - // THEN - expect(screenMock.grabScreenRegion).toBeCalledTimes(1); - expect(screenMock.grabScreenRegion).toBeCalledWith(screenRegion); - }); - - it("should delegate calls to highlightScreenRegion", async () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - const screenRegion = new Region(0, 0, 100, 100); - const opacity = 0.25; - const duration = 1; - - // WHEN - await SUT.highlightScreenRegion(screenRegion, duration, opacity); - - // THEN - expect(screenMock.highlightScreenRegion).toBeCalledTimes(1); - expect(screenMock.highlightScreenRegion).toBeCalledWith(screenRegion, duration, opacity); - }); - - it("should delegate calls to screenWidth", async () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - - // WHEN - await SUT.screenWidth(); - - // THEN - expect(screenMock.screenWidth).toBeCalledTimes(1); - }); - - it("should delegate calls to screenHeight", async () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - - // WHEN - await SUT.screenHeight(); - - // THEN - expect(screenMock.screenHeight).toBeCalledTimes(1); - }); - - it("should delegate calls to screenSize", async () => { - // GIVEN - const screenMock = new ScreenAction(); - providerRegistry.registerImageFinder(finderMock); - providerRegistry.registerScreenProvider(screenMock); - const SUT = new VisionAdapter(providerRegistry); - - // WHEN - await SUT.screenSize(); - - // THEN - expect(screenMock.screenSize).toBeCalledTimes(1); - }); - - it("should delegate calls to findImage", async () => { - // GIVEN - providerRegistry.registerImageFinder(finderMock); - const SUT = new VisionAdapter(providerRegistry); - const request = new MatchRequest( - new Image(100, 100, new ArrayBuffer(0), 3), - "foo", - new Region(0, 0, 100, 100), - 0.99, - true); - - // WHEN - await SUT.findOnScreenRegion(request); - - expect(finderMock.findMatch).toBeCalledTimes(1); - expect(finderMock.findMatch).toBeCalledWith(request); - }); -}); diff --git a/lib/adapter/vision.adapter.class.ts b/lib/adapter/vision.adapter.class.ts deleted file mode 100644 index ac8445ca..00000000 --- a/lib/adapter/vision.adapter.class.ts +++ /dev/null @@ -1,117 +0,0 @@ -import {Image} from "../image.class"; -import {MatchRequest} from "../match-request.class"; -import {MatchResult} from "../match-result.class"; -import {Region} from "../region.class"; -import {ProviderRegistry} from "../provider/provider-registry.class"; - -/** - * {@link VisionAdapter} serves as an abstraction layer for all image based interactions. - * - * This allows to provide a high level interface for image based actions, - * without having to spread (possibly) multiple dependencies all over the code. - * All actions which involve screenshots / images are bundled in this adapter. - */ -export class VisionAdapter { - /** - * {@link VisionAdapter} class constructor - * @param registry: {@link ProviderRegistry} to access providers - */ - constructor( - private registry: ProviderRegistry - ) { - } - - /** - * {@link grabScreen} will return an {@link Image} containing the current screen image - * - * @returns An {@link Image} which will contain screenshot data as well as dimensions - */ - public grabScreen(): Promise { - return this.registry.getScreen().grabScreen(); - } - - /** - * {@link grabScreenRegion} essentially does the same as grabScreen, but only returns a specified {@link Region} - * - * @param region The screen {@link Region} we want to grab - * @returns An {@link Image} which will contain screenshot data of the specified {@link Region} as well as dimensions - */ - public grabScreenRegion(region: Region): Promise { - return this.registry.getScreen().grabScreenRegion(region); - } - - /** - * {@link highlightScreenRegion} highlights a screen {@link Region} for a given duration by overlaying it with an opaque window - * - * @param region The {@link Region} to highlight - * @param duration The highlight duration - * @param opacity Overlay opacity - */ - public highlightScreenRegion(region: Region, duration: number, opacity: number): Promise { - return this.registry.getScreen().highlightScreenRegion(region, duration, opacity); - } - - /** - * {@link findOnScreenRegion} will search for a given pattern inside a {@link Region} of the main screen - * If multiple possible occurrences are found, the one with the highest probability is returned. - * For matchProbability < 0.99 the search will be performed on grayscale images. - * - * @param matchRequest A {@link MatchRequest} which holds all required matching data - * @returns {@link MatchResult} containing location and probability of a possible match - */ - public async findOnScreenRegion( - matchRequest: MatchRequest, - ): Promise { - return new Promise(async (resolve, reject) => { - try { - const matchResult = await this.registry.getImageFinder().findMatch(matchRequest); - resolve(matchResult); - } catch (e) { - reject(e); - } - }); - } - - /** - * {@link screenWidth} returns the main screens width as reported by the OS. - * Please notice that on e.g. Apples Retina display the reported width - * and the actual pixel size may differ - * - * @returns The main screens width as reported by the OS - */ - public screenWidth(): Promise { - return this.registry.getScreen().screenWidth(); - } - - /** - * {@link screenHeight} returns the main screens height as reported by the OS. - * Please notice that on e.g. Apples Retina display the reported width - * and the actual pixel size may differ - * - * @returns The main screens height as reported by the OS - */ - public screenHeight(): Promise { - return this.registry.getScreen().screenHeight(); - } - - /** - * {@link screenSize} returns a {@link Region} object with the main screens size. - * Please note that on e.g. Apples Retina display the reported width - * and the actual pixel size may differ - * - * @returns A {@link Region} object representing the size of a systems main screen - */ - public screenSize(): Promise { - return this.registry.getScreen().screenSize(); - } - - /** - * {@link saveImage} saves an {@link Image} to a given path on disk. - * - * @param image The {@link Image} to store - * @param path The path where to store the image - */ - public saveImage(image: Image, path: string): Promise { - return this.registry.getImageWriter().store({data: image, path}); - } -} diff --git a/lib/assert.class.spec.ts b/lib/assert.class.spec.ts index 087e7161..1fb6798c 100644 --- a/lib/assert.class.spec.ts +++ b/lib/assert.class.spec.ts @@ -1,19 +1,17 @@ -import {VisionAdapter} from "./adapter/vision.adapter.class"; import {AssertClass} from "./assert.class"; import {Region} from "./region.class"; import {ScreenClass} from "./screen.class"; import providerRegistry from "./provider/provider-registry.class"; -jest.mock('jimp', () => {}); -jest.mock("./adapter/native.adapter.class"); -jest.mock("./adapter/vision.adapter.class"); +jest.mock('jimp', () => { +}); jest.mock("./screen.class"); 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(new VisionAdapter(providerRegistry)); + const screenMock = new ScreenClass(providerRegistry); const SUT = new AssertClass(screenMock); const needle = "foo"; @@ -26,7 +24,7 @@ describe("Assert", () => { it("isVisible should throw if a match is found.", async () => { // GIVEN ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")); - const screenMock = new ScreenClass(new VisionAdapter(providerRegistry)); + const screenMock = new ScreenClass(providerRegistry); const SUT = new AssertClass(screenMock); const needle = "foo"; @@ -39,7 +37,7 @@ describe("Assert", () => { it("isVisible should throw if a match is found.", async () => { // GIVEN ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")); - const screenMock = new ScreenClass(new VisionAdapter(providerRegistry)); + const screenMock = new ScreenClass(providerRegistry); const SUT = new AssertClass(screenMock); const searchRegion = new Region(10, 10, 10, 10); const needle = "foo"; @@ -56,7 +54,7 @@ describe("Assert", () => { 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(new VisionAdapter(providerRegistry)); + const screenMock = new ScreenClass(providerRegistry); const SUT = new AssertClass(screenMock); const needle = "foo"; @@ -69,7 +67,7 @@ describe("Assert", () => { it("isVisible should throw if a match is found.", async () => { // GIVEN ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")); - const screenMock = new ScreenClass(new VisionAdapter(providerRegistry)); + const screenMock = new ScreenClass(providerRegistry); const SUT = new AssertClass(screenMock); const needle = "foo"; diff --git a/lib/clipboard.class.e2e.spec.ts b/lib/clipboard.class.e2e.spec.ts index c7c36f9c..d4e35f1d 100644 --- a/lib/clipboard.class.e2e.spec.ts +++ b/lib/clipboard.class.e2e.spec.ts @@ -1,11 +1,9 @@ -import {NativeAdapter} from "./adapter/native.adapter.class"; import {ClipboardClass} from "./clipboard.class"; import providerRegistry from "./provider/provider-registry.class"; describe("Clipboard class", () => { it("should paste copied input from system clipboard.", async () => { - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new ClipboardClass(adapterMock); + const SUT = new ClipboardClass(providerRegistry); const textToCopy = "bar"; diff --git a/lib/clipboard.class.spec.ts b/lib/clipboard.class.spec.ts index 19450bdb..c0119bc0 100644 --- a/lib/clipboard.class.spec.ts +++ b/lib/clipboard.class.spec.ts @@ -1,31 +1,47 @@ -import {NativeAdapter} from "./adapter/native.adapter.class"; import {ClipboardClass} from "./clipboard.class"; -import providerRegistry from "./provider/provider-registry.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; +import {mockPartial} from "sneer"; +import {ClipboardProviderInterface} from "./provider"; -jest.mock('jimp', () => {}); -jest.mock("./adapter/native.adapter.class"); +jest.mock('jimp', () => { +}); beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); -describe("Clipboard class", () => { - it("should call the native adapters copy method.", () => { - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new ClipboardClass(adapterMock); +const providerRegistryMock = mockPartial({}) +describe("Clipboard class", () => { + it("should call providers copy method.", () => { + // GIVEN + const SUT = new ClipboardClass(providerRegistryMock); + const copyMock = jest.fn(); + providerRegistryMock.getClipboard = jest.fn(() => mockPartial({ + copy: copyMock + })); const textToCopy = "bar"; + // WHEN SUT.copy(textToCopy); - expect(adapterMock.copy).toHaveBeenCalledTimes(1); - expect(adapterMock.copy).toHaveBeenCalledWith(textToCopy); + + // THEN + expect(copyMock).toHaveBeenCalledTimes(1); + expect(copyMock).toHaveBeenCalledWith(textToCopy); }); - it("should call the native adapters paste method.", () => { - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new ClipboardClass(adapterMock); + it("should call providers paste method.", () => { + // GIVEN + const SUT = new ClipboardClass(providerRegistryMock); + const pasteMock = jest.fn(); + providerRegistryMock.getClipboard = jest.fn(() => mockPartial({ + paste: pasteMock + })); + // WHEN SUT.paste(); - expect(adapterMock.paste).toHaveBeenCalledTimes(1); + + // THEN + expect(pasteMock).toHaveBeenCalledTimes(1); }); }); diff --git a/lib/clipboard.class.ts b/lib/clipboard.class.ts index 03e371c5..67aa3744 100644 --- a/lib/clipboard.class.ts +++ b/lib/clipboard.class.ts @@ -1,14 +1,14 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; - /** * {@link ClipboardClass} class gives access to a systems clipboard */ +import {ProviderRegistry} from "./provider/provider-registry.class"; + export class ClipboardClass { /** * {@link ClipboardClass} class constructor - * @param nativeAdapter {@link NativeAdapter} instance used to access OS API + * @param providerRegistry */ - constructor(private nativeAdapter: NativeAdapter) { + constructor(private providerRegistry: ProviderRegistry) { } /** @@ -16,13 +16,13 @@ export class ClipboardClass { * @param text The text to copy */ public copy(text: string): Promise { - return this.nativeAdapter.copy(text); + return this.providerRegistry.getClipboard().copy(text); } /** * {@link paste} returns the current content of the system clipboard (limited to text) */ public paste(): Promise { - return this.nativeAdapter.paste(); + return this.providerRegistry.getClipboard().paste(); } } diff --git a/lib/keyboard.class.spec.ts b/lib/keyboard.class.spec.ts index ad6edcbc..1406561e 100644 --- a/lib/keyboard.class.spec.ts +++ b/lib/keyboard.class.spec.ts @@ -1,115 +1,152 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; -import { Key } from "./key.enum"; -import { KeyboardClass } from "./keyboard.class"; -import providerRegistry from "./provider/provider-registry.class"; +import {Key} from "./key.enum"; +import {KeyboardClass} from "./keyboard.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; +import {mockPartial} from "sneer"; +import {KeyboardProviderInterface} from "./provider"; -jest.mock("./adapter/native.adapter.class"); jest.setTimeout(10000); beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); -describe("Keyboard", () => { - it("should have a default delay of 300 ms", () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - - // WHEN - - // THEN - expect(SUT.config.autoDelayMs).toEqual(300); - }); - - it("should pass input strings down to the type call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = "Test input!"; - - // WHEN - await SUT.type(payload); - - // THEN - expect(adapterMock.type).toHaveBeenCalledTimes(payload.length); - for (const char of payload.split("")) { - expect(adapterMock.type).toHaveBeenCalledWith(char); - } - }); - - it("should pass multiple input strings down to the type call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = ["Test input!", "Array test2"]; - - // WHEN - await SUT.type(...payload); - - // THEN - expect(adapterMock.type).toHaveBeenCalledTimes(payload.join(" ").length); - for (const char of payload.join(" ").split("")) { - expect(adapterMock.type).toHaveBeenCalledWith(char); - } - }); - - it("should pass input keys down to the click call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = [Key.A, Key.S, Key.D, Key.F]; - - // WHEN - await SUT.type(...payload); - - // THEN - expect(adapterMock.click).toHaveBeenCalledTimes(1); - expect(adapterMock.click).toHaveBeenCalledWith(...payload); - }); - - it("should pass a list of input keys down to the click call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = [Key.A, Key.S, Key.D, Key.F]; - - // WHEN - for (const key of payload) { - await SUT.type(key); +const providerRegistryMock = mockPartial({ + getKeyboard(): KeyboardProviderInterface { + return mockPartial({ + setKeyboardDelay: jest.fn(), + }) } +}) - // THEN - expect(adapterMock.click).toHaveBeenCalledTimes(payload.length); - }); - - it("should pass a list of input keys down to the pressKey call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = [Key.A, Key.S, Key.D, Key.F]; - - // WHEN - for (const key of payload) { - await SUT.pressKey(key); - } - - // THEN - expect(adapterMock.pressKey).toHaveBeenCalledTimes(payload.length); - }); - - it("should pass a list of input keys down to the releaseKey call.", async () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new KeyboardClass(adapterMock); - const payload = [Key.A, Key.S, Key.D, Key.F]; - - // WHEN - for (const key of payload) { - await SUT.releaseKey(key); - } - - // THEN - expect(adapterMock.releaseKey).toHaveBeenCalledTimes(payload.length); - }); +describe("Keyboard", () => { + it("should have a default delay of 300 ms", () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + + // WHEN + + // THEN + expect(SUT.config.autoDelayMs).toEqual(300); + }); + + it("should pass input strings down to the type call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = "Test input!"; + + const typeMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + type: typeMock + })); + + // WHEN + await SUT.type(payload); + + // THEN + expect(typeMock).toHaveBeenCalledTimes(payload.length); + for (const char of payload.split("")) { + expect(typeMock).toHaveBeenCalledWith(char); + } + }); + + it("should pass multiple input strings down to the type call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = ["Test input!", "Array test2"]; + + const typeMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + type: typeMock + })); + + // WHEN + await SUT.type(...payload); + + // THEN + expect(typeMock).toHaveBeenCalledTimes(payload.join(" ").length); + for (const char of payload.join(" ").split("")) { + expect(typeMock).toHaveBeenCalledWith(char); + } + }); + + it("should pass input keys down to the click call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = [Key.A, Key.S, Key.D, Key.F]; + + const clickMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + click: clickMock + })); + + // WHEN + await SUT.type(...payload); + + // THEN + expect(clickMock).toHaveBeenCalledTimes(1); + expect(clickMock).toHaveBeenCalledWith(...payload); + }); + + it("should pass a list of input keys down to the click call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = [Key.A, Key.S, Key.D, Key.F]; + + const clickMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + click: clickMock + })); + + // WHEN + for (const key of payload) { + await SUT.type(key); + } + + // THEN + expect(clickMock).toHaveBeenCalledTimes(payload.length); + }); + + it("should pass a list of input keys down to the pressKey call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = [Key.A, Key.S, Key.D, Key.F]; + + const keyMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + pressKey: keyMock + })); + + // WHEN + for (const key of payload) { + await SUT.pressKey(key); + } + + // THEN + expect(keyMock).toHaveBeenCalledTimes(payload.length); + }); + + it("should pass a list of input keys down to the releaseKey call.", async () => { + // GIVEN + const SUT = new KeyboardClass(providerRegistryMock); + const payload = [Key.A, Key.S, Key.D, Key.F]; + + const keyMock = jest.fn(); + providerRegistryMock.getKeyboard = jest.fn(() => mockPartial({ + setKeyboardDelay: jest.fn(), + releaseKey: keyMock + })); + + // WHEN + for (const key of payload) { + await SUT.releaseKey(key); + } + + // THEN + expect(keyMock).toHaveBeenCalledTimes(payload.length); + }); }); diff --git a/lib/keyboard.class.ts b/lib/keyboard.class.ts index f0383a14..669e722b 100644 --- a/lib/keyboard.class.ts +++ b/lib/keyboard.class.ts @@ -1,11 +1,11 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; -import { Key } from "./key.enum"; -import { sleep } from "./sleep.function"; +import {Key} from "./key.enum"; +import {sleep} from "./sleep.function"; +import {ProviderRegistry} from "./provider/provider-registry.class"; type StringOrKey = string[] | Key[]; const inputIsString = (input: (string | Key)[]): input is string[] => { - return input.every((elem: string | Key) => typeof elem === "string"); + return input.every((elem: string | Key) => typeof elem === "string"); }; /** @@ -13,93 +13,93 @@ const inputIsString = (input: (string | Key)[]): input is string[] => { */ export class KeyboardClass { - /** - * Config object for {@link KeyboardClass} class - */ - public config = { /** - * Configures the delay between single key events + * Config object for {@link KeyboardClass} class */ - autoDelayMs: 300, - }; + public config = { + /** + * Configures the delay between single key events + */ + autoDelayMs: 300, + }; - /** - * {@link KeyboardClass} class constructor - * @param nativeAdapter {@link NativeAdapter} instance which bundles access to mouse, keyboard and clipboard - */ - constructor(private nativeAdapter: NativeAdapter) { - this.nativeAdapter.setKeyboardDelay(this.config.autoDelayMs); - } + /** + * {@link KeyboardClass} class constructor + * @param providerRegistry + */ + constructor(private providerRegistry: ProviderRegistry) { + this.providerRegistry.getKeyboard().setKeyboardDelay(this.config.autoDelayMs); + } - /** - * {@link type} types a sequence of {@link String} or single {@link Key}s via system keyboard - * @example - * ```typescript - * await keyboard.type(Key.A, Key.S, Key.D, Key.F); - * await keyboard.type("Hello, world!"); - * ``` - * - * @param input Sequence of {@link String} or {@link Key} to type - */ - public type(...input: StringOrKey): Promise { - return new Promise(async (resolve, reject) => { - try { - if (inputIsString(input)) { - for (const char of input.join(" ").split("")) { - await sleep(this.config.autoDelayMs); - await this.nativeAdapter.type(char); - } - } else { - await this.nativeAdapter.click(...input as Key[]); - } - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link type} types a sequence of {@link String} or single {@link Key}s via system keyboard + * @example + * ```typescript + * await keyboard.type(Key.A, Key.S, Key.D, Key.F); + * await keyboard.type("Hello, world!"); + * ``` + * + * @param input Sequence of {@link String} or {@link Key} to type + */ + public type(...input: StringOrKey): Promise { + return new Promise(async (resolve, reject) => { + try { + if (inputIsString(input)) { + for (const char of input.join(" ").split("")) { + await sleep(this.config.autoDelayMs); + await this.providerRegistry.getKeyboard().type(char); + } + } else { + await this.providerRegistry.getKeyboard().click(...input as Key[]); + } + resolve(this); + } catch (e) { + reject(e); + } + }); + } - /** - * {@link pressKey} presses and holds a single {@link Key} for {@link Key} combinations - * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press - * @example - * ```typescript - * // Will press and hold key combination STRG + V - * await keyboard.pressKey(Key.STRG, Key.A); - * ``` - * - * @param keys Array of {@link Key}s to press and hold - */ - public pressKey(...keys: Key[]): Promise { - return new Promise(async (resolve, reject) => { - try { - await this.nativeAdapter.pressKey(...keys); - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link pressKey} presses and holds a single {@link Key} for {@link Key} combinations + * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press + * @example + * ```typescript + * // Will press and hold key combination STRG + V + * await keyboard.pressKey(Key.STRG, Key.A); + * ``` + * + * @param keys Array of {@link Key}s to press and hold + */ + public pressKey(...keys: Key[]): Promise { + return new Promise(async (resolve, reject) => { + try { + await this.providerRegistry.getKeyboard().pressKey(...keys); + resolve(this); + } catch (e) { + reject(e); + } + }); + } - /** - * {@link pressKey} releases a single {@link Key} for {@link Key} combinations - * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press - * @example - * ```typescript - * // Will release key combination STRG + V - * await keyboard.releaseKey(Key.STRG, Key.A); - * ``` - * - * @param keys Array of {@link Key}s to release - */ - public releaseKey(...keys: Key[]): Promise { - return new Promise(async (resolve, reject) => { - try { - await this.nativeAdapter.releaseKey(...keys); - resolve(this); - } catch (e) { - reject(e); - } - }); - } + /** + * {@link pressKey} releases a single {@link Key} for {@link Key} combinations + * Modifier {@link Key}s are to be given in "natural" ordering, so first modifier {@link Key}s, followed by the {@link Key} to press + * @example + * ```typescript + * // Will release key combination STRG + V + * await keyboard.releaseKey(Key.STRG, Key.A); + * ``` + * + * @param keys Array of {@link Key}s to release + */ + public releaseKey(...keys: Key[]): Promise { + return new Promise(async (resolve, reject) => { + try { + await this.providerRegistry.getKeyboard().releaseKey(...keys); + resolve(this); + } catch (e) { + reject(e); + } + }); + } } diff --git a/lib/mouse.class.spec.ts b/lib/mouse.class.spec.ts index 949889ac..6302afb3 100644 --- a/lib/mouse.class.spec.ts +++ b/lib/mouse.class.spec.ts @@ -1,156 +1,217 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; -import { Button } from "./button.enum"; -import { MouseClass } from "./mouse.class"; -import { Point } from "./point.class"; -import { LineHelper } from "./util/linehelper.class"; -import providerRegistry from "./provider/provider-registry.class"; - -jest.mock("./adapter/native.adapter.class"); +import {Button} from "./button.enum"; +import {MouseClass} from "./mouse.class"; +import {Point} from "./point.class"; +import {LineHelper} from "./util/linehelper.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; +import {mockPartial} from "sneer"; +import {MouseProviderInterface} from "./provider"; beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); const linehelper = new LineHelper(); +const providerRegistryMock = mockPartial({ + getMouse(): MouseProviderInterface { + return mockPartial({ + setMouseDelay: jest.fn() + }) + } +}); + describe("Mouse class", () => { - it("should have a default delay of 500 ms", () => { - // GIVEN - const adapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(adapterMock); - - // WHEN - - // THEN - expect(SUT.config.autoDelayMs).toEqual(100); - }); - - it("should forward scrollLeft to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const scrollAmount = 5; - - // WHEN - const result = await SUT.scrollLeft(scrollAmount); - - // THEN - expect(nativeAdapterMock.scrollLeft).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); - - it("should forward scrollRight to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const scrollAmount = 5; - - // WHEN - const result = await SUT.scrollRight(scrollAmount); - - // THEN - expect(nativeAdapterMock.scrollRight).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); - - it("should forward scrollDown to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const scrollAmount = 5; - - // WHEN - const result = await SUT.scrollDown(scrollAmount); - - // THEN - expect(nativeAdapterMock.scrollDown).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); - - it("should forward scrollUp to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const scrollAmount = 5; - - // WHEN - const result = await SUT.scrollUp(scrollAmount); - - // THEN - expect(nativeAdapterMock.scrollUp).toBeCalledWith(scrollAmount); - expect(result).toBe(SUT); - }); - - it("should forward leftClick to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - - // WHEN - const result = await SUT.leftClick(); - - // THEN - expect(nativeAdapterMock.leftClick).toBeCalled(); - expect(result).toBe(SUT); - }); - - it("should forward rightClick to the native adapter class", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - - // WHEN - const result = await SUT.rightClick(); - - // THEN - expect(nativeAdapterMock.rightClick).toBeCalled(); - expect(result).toBe(SUT); - }); - - it("update mouse position along path on move", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10)); - - // WHEN - const result = await SUT.move(path); - - // THEN - expect(nativeAdapterMock.setMousePosition).toBeCalledTimes(path.length); - expect(result).toBe(SUT); - }); - - it("should press and hold left mouse button, move and release left mouse button on drag", async () => { - // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10)); - - // WHEN - const result = await SUT.drag(path); - - // THEN - expect(nativeAdapterMock.pressButton).toBeCalledWith(Button.LEFT); - expect(nativeAdapterMock.setMousePosition).toBeCalledTimes(path.length); - expect(nativeAdapterMock.releaseButton).toBeCalledWith(Button.LEFT); - expect(result).toBe(SUT); - }); + it("should have a default delay of 500 ms", () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + + // WHEN + + // THEN + expect(SUT.config.autoDelayMs).toEqual(100); + }); + + it("should forward scrollLeft to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const scrollAmount = 5; + + const scrollMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + scrollLeft: scrollMock + })); + + // WHEN + const result = await SUT.scrollLeft(scrollAmount); + + // THEN + expect(scrollMock).toBeCalledWith(scrollAmount); + expect(result).toBe(SUT); + }); + + it("should forward scrollRight to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const scrollAmount = 5; + + const scrollMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + scrollRight: scrollMock + })); + + // WHEN + const result = await SUT.scrollRight(scrollAmount); + + // THEN + expect(scrollMock).toBeCalledWith(scrollAmount); + expect(result).toBe(SUT); + }); + + it("should forward scrollDown to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const scrollAmount = 5; + + const scrollMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + scrollDown: scrollMock + })); + + // WHEN + const result = await SUT.scrollDown(scrollAmount); + + // THEN + expect(scrollMock).toBeCalledWith(scrollAmount); + expect(result).toBe(SUT); + }); + + it("should forward scrollUp to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const scrollAmount = 5; + + const scrollMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + scrollUp: scrollMock + })); + + // WHEN + const result = await SUT.scrollUp(scrollAmount); + + // THEN + expect(scrollMock).toBeCalledWith(scrollAmount); + expect(result).toBe(SUT); + }); + + it("should forward leftClick to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + leftClick: clickMock + })); + + // WHEN + const result = await SUT.leftClick(); + + // THEN + expect(clickMock).toBeCalled(); + expect(result).toBe(SUT); + }); + + it("should forward rightClick to the provider", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + + const clickMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + rightClick: clickMock + })); + + // WHEN + const result = await SUT.rightClick(); + + // THEN + expect(clickMock).toBeCalled(); + expect(result).toBe(SUT); + }); + + it("update mouse position along path on move", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10)); + + const setPositionMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + setMousePosition: setPositionMock + })); + + // WHEN + const result = await SUT.move(path); + + // THEN + expect(setPositionMock).toBeCalledTimes(path.length); + expect(result).toBe(SUT); + }); + + it("should press and hold left mouse button, move and release left mouse button on drag", async () => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const path = linehelper.straightLine(new Point(0, 0), new Point(10, 10)); + + const setPositionMock = jest.fn(); + const pressButtonMock = jest.fn(); + const releaseButtonMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + setMousePosition: setPositionMock, + pressButton: pressButtonMock, + releaseButton: releaseButtonMock + })); + + // WHEN + const result = await SUT.drag(path); + + // THEN + expect(pressButtonMock).toBeCalledWith(Button.LEFT); + expect(setPositionMock).toBeCalledTimes(path.length); + expect(releaseButtonMock).toBeCalledWith(Button.LEFT); + expect(result).toBe(SUT); + }); }); describe("Mousebuttons", () => { - it.each([ - [Button.LEFT, Button.LEFT], - [Button.MIDDLE, Button.MIDDLE], - [Button.RIGHT, Button.RIGHT], - ] as Array<[Button, Button]>)("should be pressed and released", async (input: Button, expected: Button) => { - const nativeAdapterMock = new NativeAdapter(providerRegistry); - const SUT = new MouseClass(nativeAdapterMock); - const pressed = await SUT.pressButton(input); - const released = await SUT.releaseButton(input); - expect(nativeAdapterMock.pressButton).toBeCalledWith(expected); - expect(nativeAdapterMock.releaseButton).toBeCalledWith(expected); - expect(pressed).toBe(SUT); - expect(released).toBe(SUT); - }); + it.each([ + [Button.LEFT, Button.LEFT], + [Button.MIDDLE, Button.MIDDLE], + [Button.RIGHT, Button.RIGHT], + ] as Array<[Button, Button]>)("should be pressed and released", async (input: Button, expected: Button) => { + // GIVEN + const SUT = new MouseClass(providerRegistryMock); + const pressButtonMock = jest.fn(); + const releaseButtonMock = jest.fn(); + providerRegistryMock.getMouse = jest.fn(() => mockPartial({ + setMouseDelay: jest.fn(), + pressButton: pressButtonMock, + releaseButton: releaseButtonMock + })); + + // WHEN + const pressed = await SUT.pressButton(input); + const released = await SUT.releaseButton(input); + + // THEN + expect(pressButtonMock).toBeCalledWith(expected); + expect(releaseButtonMock).toBeCalledWith(expected); + expect(pressed).toBe(SUT); + expect(released).toBe(SUT); + }); }); diff --git a/lib/mouse.class.ts b/lib/mouse.class.ts index e1ad0649..39c902ba 100644 --- a/lib/mouse.class.ts +++ b/lib/mouse.class.ts @@ -1,8 +1,8 @@ -import {NativeAdapter} from "./adapter/native.adapter.class"; import {Button} from "./button.enum"; import {Point} from "./point.class"; import {busyWaitForNanoSeconds, sleep} from "./sleep.function"; import {calculateMovementTimesteps, EasingFunction, linear} from "./mouse-movement.function"; +import {ProviderRegistry} from "./provider/provider-registry.class"; /** * {@link MouseClass} class provides methods to emulate mouse input @@ -25,10 +25,10 @@ export class MouseClass { /** * {@link MouseClass} class constructor - * @param nativeAdapter {@link NativeAdapter} instance which bundles access to mouse, keyboard and clipboard + * @param providerRegistry */ - constructor(private nativeAdapter: NativeAdapter) { - this.nativeAdapter.setMouseDelay(0); + constructor(private providerRegistry: ProviderRegistry) { + this.providerRegistry.getMouse().setMouseDelay(0); } /** @@ -38,7 +38,7 @@ export class MouseClass { public async setPosition(target: Point): Promise { return new Promise(async (resolve, reject) => { try { - await this.nativeAdapter.setMousePosition(target); + await this.providerRegistry.getMouse().setMousePosition(target); resolve(this); } catch (e) { reject(e); @@ -50,7 +50,7 @@ export class MouseClass { * {@link getPosition} returns a {@link Point} representing the current mouse position */ public getPosition(): Promise { - return this.nativeAdapter.currentMousePosition(); + return this.providerRegistry.getMouse().currentMousePosition(); } /** @@ -67,7 +67,7 @@ export class MouseClass { const node = pathSteps[idx]; const minTime = timeSteps[idx]; await busyWaitForNanoSeconds(minTime); - await this.nativeAdapter.setMousePosition(node); + await this.providerRegistry.getMouse().setMousePosition(node); } resolve(this); } catch (e) { @@ -82,7 +82,7 @@ export class MouseClass { public async leftClick(): Promise { return new Promise(async resolve => { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.leftClick(); + await this.providerRegistry.getMouse().leftClick(); resolve(this); }); } @@ -94,7 +94,7 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.rightClick(); + await this.providerRegistry.getMouse().rightClick(); resolve(this); } catch (e) { reject(e); @@ -111,7 +111,7 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.scrollDown(amount); + await this.providerRegistry.getMouse().scrollDown(amount); resolve(this); } catch (e) { reject(e); @@ -128,7 +128,7 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.scrollUp(amount); + await this.providerRegistry.getMouse().scrollUp(amount); resolve(this); } catch (e) { reject(e); @@ -145,7 +145,7 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.scrollLeft(amount); + await this.providerRegistry.getMouse().scrollLeft(amount); resolve(this); } catch (e) { reject(e); @@ -162,7 +162,7 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.scrollRight(amount); + await this.providerRegistry.getMouse().scrollRight(amount); resolve(this); } catch (e) { reject(e); @@ -179,9 +179,9 @@ export class MouseClass { return new Promise(async (resolve, reject) => { try { await sleep(this.config.autoDelayMs); - await this.nativeAdapter.pressButton(Button.LEFT); + await this.providerRegistry.getMouse().pressButton(Button.LEFT); await this.move(path); - await this.nativeAdapter.releaseButton(Button.LEFT); + await this.providerRegistry.getMouse().releaseButton(Button.LEFT); resolve(this); } catch (e) { reject(e); @@ -196,7 +196,7 @@ export class MouseClass { public async pressButton(btn: Button): Promise { return new Promise(async (resolve, reject) => { try { - await this.nativeAdapter.pressButton(btn); + await this.providerRegistry.getMouse().pressButton(btn); resolve(this); } catch (e) { reject(e); @@ -211,7 +211,7 @@ export class MouseClass { public async releaseButton(btn: Button): Promise { return new Promise(async (resolve, reject) => { try { - await this.nativeAdapter.releaseButton(btn); + await this.providerRegistry.getMouse().releaseButton(btn); resolve(this); } catch (e) { reject(e); diff --git a/lib/movement.function.ts b/lib/movement.function.ts index 5c3feb1d..be8b2adf 100644 --- a/lib/movement.function.ts +++ b/lib/movement.function.ts @@ -1,30 +1,30 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; -import { MovementApi } from "./movement-api.interface"; -import { Point } from "./point.class"; -import { LineHelper } from "./util/linehelper.class"; +import {MovementApi} from "./movement-api.interface"; +import {Point} from "./point.class"; +import {LineHelper} from "./util/linehelper.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; -export const createMovementApi = (native: NativeAdapter, lineHelper: LineHelper): MovementApi => { - return ({ - down: async (px: number): Promise => { - const pos = await native.currentMousePosition(); - return lineHelper.straightLine(pos, new Point(pos.x, pos.y + px)); - }, - left: async (px: number): Promise => { - const pos = await native.currentMousePosition(); - return lineHelper.straightLine(pos, new Point(pos.x - px, pos.y)); - }, - right: async (px: number): Promise => { - const pos = await native.currentMousePosition(); - return lineHelper.straightLine(pos, new Point(pos.x + px, pos.y)); - }, - straightTo: async (target: Point | Promise): Promise => { - const targetPoint = await target; - const origin = await native.currentMousePosition(); - return lineHelper.straightLine(origin, targetPoint); - }, - up: async (px: number): Promise => { - const pos = await native.currentMousePosition(); - return lineHelper.straightLine(pos, new Point(pos.x, pos.y - px)); - }, - }); +export const createMovementApi = (providerRegistry: ProviderRegistry, lineHelper: LineHelper): MovementApi => { + return ({ + down: async (px: number): Promise => { + const pos = await providerRegistry.getMouse().currentMousePosition(); + return lineHelper.straightLine(pos, new Point(pos.x, pos.y + px)); + }, + left: async (px: number): Promise => { + const pos = await providerRegistry.getMouse().currentMousePosition(); + return lineHelper.straightLine(pos, new Point(pos.x - px, pos.y)); + }, + right: async (px: number): Promise => { + const pos = await providerRegistry.getMouse().currentMousePosition(); + return lineHelper.straightLine(pos, new Point(pos.x + px, pos.y)); + }, + straightTo: async (target: Point | Promise): Promise => { + const targetPoint = await target; + const origin = await providerRegistry.getMouse().currentMousePosition(); + return lineHelper.straightLine(origin, targetPoint); + }, + up: async (px: number): Promise => { + const pos = await providerRegistry.getMouse().currentMousePosition(); + return lineHelper.straightLine(pos, new Point(pos.x, pos.y - px)); + }, + }); }; diff --git a/lib/screen.class.spec.ts b/lib/screen.class.spec.ts index 74077ade..c1400957 100644 --- a/lib/screen.class.spec.ts +++ b/lib/screen.class.spec.ts @@ -1,46 +1,49 @@ -import { join } from "path"; -import { cwd } from "process"; -import { VisionAdapter } from "./adapter/vision.adapter.class"; -import { Image } from "./image.class"; -import { LocationParameters } from "./locationparameters.class"; -import { MatchRequest } from "./match-request.class"; -import { MatchResult } from "./match-result.class"; -import { Region } from "./region.class"; -import { ScreenClass } from "./screen.class"; -import { mockPartial } from "sneer"; -import { FileType } from "./file-type.enum"; -import providerRegistry from "./provider/provider-registry.class"; - -jest.mock('jimp', () => {}); -jest.mock("./adapter/native.adapter.class"); -jest.mock("./adapter/vision.adapter.class"); +import {join} from "path"; +import {cwd} from "process"; +import {Image} from "./image.class"; +import {LocationParameters} from "./locationparameters.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, ScreenProviderInterface} from "./provider"; + +jest.mock('jimp', () => { +}); const searchRegion = new Region(0, 0, 1000, 1000); -beforeAll(() => { - VisionAdapter.prototype.grabScreen = jest.fn(() => { - return Promise.resolve(new Image(searchRegion.width, searchRegion.height, new ArrayBuffer(0), 3)); - }); +const providerRegistryMock = mockPartial({ + getScreen(): ScreenProviderInterface { + return mockPartial({ + grabScreen(): Promise { + return Promise.resolve(new Image(searchRegion.width, searchRegion.height, new ArrayBuffer(0), 3)); + }, + screenSize(): Promise { + return Promise.resolve(searchRegion); + } + }) + } +}); - VisionAdapter.prototype.screenSize = jest.fn(() => { - return Promise.resolve(searchRegion); - }); +beforeEach(() => { + jest.resetAllMocks(); }); describe("Screen.", () => { - describe("find", () => { it("should resolve with sufficient confidence.", async () => { - // GIVEN const matchResult = new MatchResult(0.99, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); // WHEN const resultRegion = SUT.find(imagePath); @@ -53,19 +56,18 @@ describe("Screen.", () => { searchRegion, SUT.config.confidence, true); - expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(matchRequest); + expect(findMatchMock).toHaveBeenCalledWith(matchRequest); }); it("should call registered hook before resolve", async () => { - // GIVEN const matchResult = new MatchResult(0.99, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const testCallback = jest.fn(() => Promise.resolve()); const imagePath = "test/path/to/image.png"; SUT.on(imagePath, testCallback); @@ -79,15 +81,14 @@ describe("Screen.", () => { }); it("should call multiple registered hooks before resolve", async () => { - // GIVEN const matchResult = new MatchResult(0.99, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const testCallback = jest.fn(() => Promise.resolve()); const secondCallback = jest.fn(() => Promise.resolve()); const imagePath = "test/path/to/image.png"; @@ -108,14 +109,12 @@ describe("Screen.", () => { // GIVEN const matchResult = new MatchResult(0.8, searchRegion); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - - const visionAdapterMock = new VisionAdapter(providerRegistry); - - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; // WHEN @@ -131,13 +130,12 @@ describe("Screen.", () => { // GIVEN const rejectionReason = "Search failed."; - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.reject(rejectionReason); - }); - - const visionAdapterMock = new VisionAdapter(providerRegistry); + const findMatchMock = jest.fn(() => Promise.reject(rejectionReason)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; // WHEN @@ -147,8 +145,6 @@ describe("Screen.", () => { await expect(resultRegion) .rejects .toEqual(`Searching for ${imagePath} failed. Reason: '${rejectionReason}'`); - - }); it("should override default confidence value with parameter.", async () => { @@ -157,13 +153,12 @@ describe("Screen.", () => { const minMatch = 0.8; const matchResult = new MatchResult(minMatch, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - - const visionAdapterMock = new VisionAdapter(providerRegistry); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); - const SUT = new ScreenClass(visionAdapterMock); + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; const parameters = new LocationParameters(undefined, minMatch); @@ -179,18 +174,21 @@ describe("Screen.", () => { searchRegion, minMatch, true); - expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(matchRequest); + expect(findMatchMock).toHaveBeenCalledWith(matchRequest); }); it("should override default search region with parameter.", async () => { // GIVEN const customSearchRegion = new Region(10, 10, 90, 90); const matchResult = new MatchResult(0.99, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); + + const SUT = new ScreenClass(providerRegistryMock); + const imagePath = "test/path/to/image.png"; const parameters = new LocationParameters(customSearchRegion); const expectedMatchRequest = new MatchRequest( @@ -204,17 +202,18 @@ describe("Screen.", () => { await SUT.find(imagePath, parameters); // THEN - expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest); + expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest); }); it("should override searchMultipleScales with parameter.", async () => { // GIVEN const matchResult = new MatchResult(0.99, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); + + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; const parameters = new LocationParameters(searchRegion, undefined, false); const expectedMatchRequest = new MatchRequest( @@ -228,7 +227,7 @@ describe("Screen.", () => { await SUT.find(imagePath, parameters); // THEN - expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest); + expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest); }); it("should override both confidence and search region with parameter.", async () => { @@ -236,11 +235,12 @@ describe("Screen.", () => { const minMatch = 0.8; const customSearchRegion = new Region(10, 10, 90, 90); const matchResult = new MatchResult(minMatch, searchRegion); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); + + const SUT = new ScreenClass(providerRegistryMock); const imagePath = "test/path/to/image.png"; const parameters = new LocationParameters(customSearchRegion, minMatch); const expectedMatchRequest = new MatchRequest( @@ -254,11 +254,10 @@ describe("Screen.", () => { await SUT.find(imagePath, parameters); // THEN - expect(visionAdapterMock.findOnScreenRegion).toHaveBeenCalledWith(expectedMatchRequest); + expect(findMatchMock).toHaveBeenCalledWith(expectedMatchRequest); }); it("should add search region offset to result image location", async () => { - // GIVEN const limitedSearchRegion = new Region(100, 200, 300, 400); const resultRegion = new Region(50, 100, 150, 200); @@ -270,11 +269,12 @@ describe("Screen.", () => { resultRegion.width, resultRegion.height); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); - const SUT = new ScreenClass(new VisionAdapter(providerRegistry)); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); + const SUT = new ScreenClass(providerRegistryMock); // WHEN const matchRegion = await SUT.find( "test/path/to/image.png", @@ -303,18 +303,16 @@ describe("Screen.", () => { ["with NaN y coordinate", new Region(0, "a" as unknown as number, 100, 600)], ["with NaN on width", new Region(0, 0, "a" as unknown as number, 100)], ["with NaN on height", new Region(0, 0, 100, "a" as unknown as number)], - ])("should reject search regions %s", async (_, region) =>{ - + ])("should reject search regions %s", async (_, region) => { // GIVEN const imagePath = "test/path/to/image.png" - const visionAdapterMock = new VisionAdapter(providerRegistry); - - const SUT = new ScreenClass(visionAdapterMock); - const matchResult = new MatchResult(0.99, region); - VisionAdapter.prototype.findOnScreenRegion = jest.fn(() => { - return Promise.resolve(matchResult); - }); + const findMatchMock = jest.fn(() => Promise.resolve(matchResult)); + providerRegistryMock.getImageFinder = jest.fn(() => mockPartial({ + findMatch: findMatchMock + })); + + const SUT = new ScreenClass(providerRegistryMock); // WHEN const findPromise = SUT.find( @@ -328,14 +326,15 @@ describe("Screen.", () => { }) }); - it("should return region to highlight for chaining", async () => { // GIVEN const highlightRegion = new Region(10, 20, 30, 40); - VisionAdapter.prototype.highlightScreenRegion = jest.fn(); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + const highlightMock = jest.fn((value: any) => Promise.resolve(value)); + providerRegistryMock.getScreen = jest.fn(() => mockPartial({ + highlightScreenRegion: highlightMock + })); + const SUT = new ScreenClass(providerRegistryMock); // WHEN const result = await SUT.highlight(highlightRegion); @@ -347,9 +346,12 @@ describe("Screen.", () => { // GIVEN const highlightRegion = new Region(10, 20, 30, 40); const highlightRegionPromise = new Promise(res => res(highlightRegion)); - VisionAdapter.prototype.highlightScreenRegion = jest.fn(); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + const highlightMock = jest.fn((value: any) => Promise.resolve(value)); + providerRegistryMock.getScreen = jest.fn(() => mockPartial({ + highlightScreenRegion: highlightMock + })); + + const SUT = new ScreenClass(providerRegistryMock); // WHEN const result = await SUT.highlight(highlightRegionPromise); @@ -358,15 +360,20 @@ describe("Screen.", () => { expect(result).toEqual(highlightRegion); }); - describe("capture",() => { - it("should capture the whole screen and save image", async() => { - + describe("capture", () => { + it("should capture the whole screen and save image", async () => { // GIVEN const screenshot = mockPartial({data: "pretty pretty image"}); - VisionAdapter.prototype.grabScreen = jest.fn(() => Promise.resolve(screenshot)); - VisionAdapter.prototype.saveImage = jest.fn(); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + 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) @@ -375,39 +382,26 @@ describe("Screen.", () => { // THEN expect(imagePath).toBe(expectedImagePath) - expect(VisionAdapter.prototype.grabScreen).toHaveBeenCalled() - expect(VisionAdapter.prototype.saveImage).toHaveBeenCalledWith(screenshot,expectedImagePath) - }) - - it("should consider output configuration", async () => { - - // GIVEN - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); - const imageName = "foobar" - const filePath = "/path/to/file" - const prefix = "answer_" - const postfix = "_42" - const expectedImagePath = join(filePath, `${prefix}${imageName}${postfix}${FileType.JPG.toString()}`) - - // WHEN - const imagePath = await SUT.capture(imageName, FileType.JPG, filePath, prefix, postfix) - - // THEN - expect(imagePath).toBe(expectedImagePath) - }) + expect(grabScreenMock).toHaveBeenCalled() + expect(saveImageMock).toHaveBeenCalledWith({data: screenshot, path: expectedImagePath}) + }); }) describe("captureRegion", () => { - it("should capture the specified region of the screen and save image", async () => { // GIVEN const screenshot = mockPartial({data: "pretty partial image"}); - const regionToCapture = mockPartial({top:42, left:9, height: 10, width: 3.14159265359}) - VisionAdapter.prototype.grabScreenRegion = jest.fn(() => Promise.resolve(screenshot)); - VisionAdapter.prototype.saveImage = jest.fn(); - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); + 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) @@ -416,27 +410,8 @@ describe("Screen.", () => { // THEN expect(imagePath).toBe(expectedImagePath) - expect(VisionAdapter.prototype.grabScreenRegion).toHaveBeenCalledWith(regionToCapture) - expect(VisionAdapter.prototype.saveImage).toHaveBeenCalledWith(screenshot,expectedImagePath) - }) - - it("should consider output configuration", async () => { - - // GIVEN - const regionToCapture = mockPartial({top:42, left:9, height: 10, width: 3.14159265359}) - const visionAdapterMock = new VisionAdapter(providerRegistry); - const SUT = new ScreenClass(visionAdapterMock); - const imageName = "foobar" - const filePath = "/path/to/file" - const prefix = "answer_" - const postfix = "_42" - const expectedImagePath = join(filePath, `${prefix}${imageName}${postfix}${FileType.JPG.toString()}`) - - // WHEN - const imagePath = await SUT.captureRegion(imageName, regionToCapture, FileType.JPG, filePath, prefix, postfix) - - // THEN - expect(imagePath).toBe(expectedImagePath) - }) - }) + expect(grabScreenMock).toHaveBeenCalledWith(regionToCapture) + expect(saveImageMock).toHaveBeenCalledWith({data: screenshot, path: expectedImagePath}) + }); + }); }); diff --git a/lib/screen.class.ts b/lib/screen.class.ts index ac557684..bb643180 100644 --- a/lib/screen.class.ts +++ b/lib/screen.class.ts @@ -1,6 +1,5 @@ import {join, normalize} from "path"; import {cwd} from "process"; -import {VisionAdapter} from "./adapter/vision.adapter.class"; import {FileType} from "./file-type.enum"; import {generateOutputPath} from "./generate-output-path.function"; import {LocationParameters} from "./locationparameters.class"; @@ -9,6 +8,7 @@ import {MatchResult} from "./match-result.class"; import {Region} from "./region.class"; import {timeout} from "./util/timeout.function"; import {Image} from "./image.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; export type FindHookCallback = (target: MatchResult) => Promise; @@ -48,11 +48,11 @@ export class ScreenClass { /** * {@link ScreenClass} class constructor - * @param vision {@link VisionAdapter} instance which bundles access to screen and / or computer vision related APIs + * @param providerRegistry A {@link ProviderRegistry} used to access underlying implementations * @param findHooks A {@link Map} of {@link FindHookCallback} methods assigned to a template image filename */ constructor( - private vision: VisionAdapter, + private providerRegistry: ProviderRegistry, private findHooks: Map = new Map()) { } @@ -62,7 +62,7 @@ export class ScreenClass { * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher width in in actual pixels */ public width() { - return this.vision.screenWidth(); + return this.providerRegistry.getScreen().screenWidth(); } /** @@ -71,7 +71,7 @@ export class ScreenClass { * Screens with higher pixel density (e.g. retina displays in MacBooks) might have a higher height in in actual pixels */ public height() { - return this.vision.screenHeight(); + return this.providerRegistry.getScreen().screenHeight(); } /** @@ -84,13 +84,13 @@ export class ScreenClass { params?: LocationParameters, ): Promise { const minMatch = (params && params.confidence) || this.config.confidence; - const screenSize = await this.vision.screenSize(); + const screenSize = await this.providerRegistry.getScreen().screenSize(); const searchRegion = (params && params.searchRegion) || screenSize; const searchMultipleScales = (params && params.searchMultipleScales) const fullPathToNeedle = normalize(join(this.config.resourceDirectory, templateImageFilename)); - const screenImage = await this.vision.grabScreen(); + const screenImage = await this.providerRegistry.getScreen().grabScreen(); const matchRequest = new MatchRequest( screenImage, @@ -118,7 +118,7 @@ export class ScreenClass { return new Promise(async (resolve, reject) => { try { validateSearchRegion(searchRegion, screenSize); - const matchResult = await this.vision.findOnScreenRegion(matchRequest); + const matchResult = await this.providerRegistry.getImageFinder().findMatch(matchRequest); if (matchResult.confidence >= minMatch) { const possibleHooks = this.findHooks.get(templateImageFilename) || []; for (const hook of possibleHooks) { @@ -156,7 +156,7 @@ export class ScreenClass { */ public async highlight(regionToHighlight: Region | Promise): Promise { const highlightRegion = await regionToHighlight; - await this.vision.highlightScreenRegion(highlightRegion, this.config.highlightDurationMs, this.config.highlightOpacity); + await this.providerRegistry.getScreen().highlightScreenRegion(highlightRegion, this.config.highlightDurationMs, this.config.highlightOpacity); return highlightRegion; } @@ -198,7 +198,7 @@ export class ScreenClass { filePath: string = cwd(), fileNamePrefix: string = "", fileNamePostfix: string = ""): Promise { - const currentScreen = await this.vision.grabScreen(); + const currentScreen = await this.providerRegistry.getScreen().grabScreen(); return this.saveImage( currentScreen, fileName, @@ -212,7 +212,7 @@ export class ScreenClass { * {@link grab} grabs screen content of a systems main display */ public async grab(): Promise { - return this.vision.grabScreen(); + return this.providerRegistry.getScreen().grabScreen(); } /** @@ -231,7 +231,7 @@ export class ScreenClass { filePath: string = cwd(), fileNamePrefix: string = "", fileNamePostfix: string = ""): Promise { - const regionImage = await this.vision.grabScreenRegion(await regionToCapture); + const regionImage = await this.providerRegistry.getScreen().grabScreenRegion(await regionToCapture); return this.saveImage( regionImage, fileName, @@ -246,7 +246,7 @@ export class ScreenClass { * @param regionToGrab The screen region to grab */ public async grabRegion(regionToGrab: Region | Promise): Promise { - return this.vision.grabScreenRegion(await regionToGrab); + return this.providerRegistry.getScreen().grabScreenRegion(await regionToGrab); } private async saveImage( @@ -262,7 +262,7 @@ export class ScreenClass { prefix: fileNamePrefix, type: fileFormat, }); - await this.vision.saveImage(image, outputPath); + await this.providerRegistry.getImageWriter().store({data: image, path: outputPath}) return outputPath; } } diff --git a/lib/window.class.spec.ts b/lib/window.class.spec.ts index 3f3c36af..b7cbb42d 100644 --- a/lib/window.class.spec.ts +++ b/lib/window.class.spec.ts @@ -1,36 +1,51 @@ import {Window} from "./window.class"; -import {NativeAdapter} from "./adapter/native.adapter.class"; -import providerRegistry from "./provider/provider-registry.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; +import {mockPartial} from "sneer"; +import {WindowProviderInterface} from "./provider"; -jest.mock('jimp', () => {}); -jest.mock("./adapter/native.adapter.class"); +jest.mock('jimp', () => { +}); describe("Window class", () => { - it("should retrieve the window region via its native adapter", async () => { + it("should retrieve the window region via provider", async () => { // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); + const windowMock = jest.fn(); + const providerRegistryMock = mockPartial({ + getWindow(): WindowProviderInterface { + return mockPartial({ + getWindowRegion: windowMock + }) + } + }) const mockWindowHandle = 123; - const SUT = new Window(nativeAdapterMock, mockWindowHandle); + const SUT = new Window(providerRegistryMock, mockWindowHandle); // WHEN await SUT.region // THEN - expect(nativeAdapterMock.getWindowRegion).toBeCalledTimes(1); - expect(nativeAdapterMock.getWindowRegion).toBeCalledWith(mockWindowHandle); + expect(windowMock).toBeCalledTimes(1); + expect(windowMock).toBeCalledWith(mockWindowHandle); }); - it("should retrieve the window title via its native adapter", async () => { + it("should retrieve the window title via provider", async () => { // GIVEN - const nativeAdapterMock = new NativeAdapter(providerRegistry); + const windowMock = jest.fn(); + const providerRegistryMock = mockPartial({ + getWindow(): WindowProviderInterface { + return mockPartial({ + getWindowTitle: windowMock + }) + } + }) const mockWindowHandle = 123; - const SUT = new Window(nativeAdapterMock, mockWindowHandle); + const SUT = new Window(providerRegistryMock, mockWindowHandle); // WHEN await SUT.title // THEN - expect(nativeAdapterMock.getWindowTitle).toBeCalledTimes(1); - expect(nativeAdapterMock.getWindowTitle).toBeCalledWith(mockWindowHandle); + expect(windowMock).toBeCalledTimes(1); + expect(windowMock).toBeCalledWith(mockWindowHandle); }); }); \ No newline at end of file diff --git a/lib/window.class.ts b/lib/window.class.ts index f403a73b..834b6583 100644 --- a/lib/window.class.ts +++ b/lib/window.class.ts @@ -1,15 +1,15 @@ -import { NativeAdapter } from "./adapter/native.adapter.class"; -import { Region } from "./region.class"; +import {Region} from "./region.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; export class Window { - constructor(private nativeActions: NativeAdapter, private windowHandle: number) { - } + constructor(private providerRegistry: ProviderRegistry, private windowHandle: number) { + } - get title(): Promise { - return this.nativeActions.getWindowTitle(this.windowHandle); - } + get title(): Promise { + return this.providerRegistry.getWindow().getWindowTitle(this.windowHandle); + } - get region(): Promise { - return this.nativeActions.getWindowRegion(this.windowHandle); - } + get region(): Promise { + return this.providerRegistry.getWindow().getWindowRegion(this.windowHandle); + } } \ No newline at end of file diff --git a/lib/window.function.spec.ts b/lib/window.function.spec.ts index 4b50c2ff..e4df42f9 100644 --- a/lib/window.function.spec.ts +++ b/lib/window.function.spec.ts @@ -1,5 +1,4 @@ import {createWindowApi} from "./window.function"; -import {NativeAdapter} from "./adapter/native.adapter.class"; import {Window} from "./window.class"; import providerRegistry from "./provider/provider-registry.class"; @@ -9,7 +8,7 @@ describe("WindowApi", () => { describe("getWindows", () => { it("should return a list of open Windows", async () => { // GIVEN - const SUT = createWindowApi(new NativeAdapter(providerRegistry)); + const SUT = createWindowApi(providerRegistry); // WHEN const windows = await SUT.getWindows() @@ -24,7 +23,7 @@ describe("WindowApi", () => { describe("getActiveWindow", () => { it("should return the a single Window which is currently active", async () => { // GIVEN - const SUT = createWindowApi(new NativeAdapter(providerRegistry)); + const SUT = createWindowApi(providerRegistry); // WHEN const window = await SUT.getActiveWindow(); diff --git a/lib/window.function.ts b/lib/window.function.ts index a88cbc78..2aba056a 100644 --- a/lib/window.function.ts +++ b/lib/window.function.ts @@ -1,17 +1,17 @@ import { WindowApi } from "./window-api.interface"; -import { NativeAdapter } from "./adapter/native.adapter.class"; import { Window } from "./window.class"; +import {ProviderRegistry} from "./provider/provider-registry.class"; -export const createWindowApi = (nativeAdapter: NativeAdapter): WindowApi => { +export const createWindowApi = (providerRegistry: ProviderRegistry): WindowApi => { return ({ async getActiveWindow(): Promise { - const windowHandle = await nativeAdapter.getActiveWindow(); - return new Window(nativeAdapter, windowHandle); + const windowHandle = await providerRegistry.getWindow().getActiveWindow(); + return new Window(providerRegistry, windowHandle); }, async getWindows(): Promise { - const windowHandles = await nativeAdapter.getWindows(); + const windowHandles = await providerRegistry.getWindow().getWindows(); return windowHandles.map((handle: number) => { - return new Window(nativeAdapter, handle); + return new Window(providerRegistry, handle); }); }, });