Skip to content

Commit

Permalink
Implemented find, findAll, waitFor and hooks for windows
Browse files Browse the repository at this point in the history
  • Loading branch information
s1hofmann committed Apr 1, 2024
1 parent f47b91a commit 1690171
Show file tree
Hide file tree
Showing 3 changed files with 346 additions and 4 deletions.
174 changes: 172 additions & 2 deletions core/nut.js/lib/window.class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Window } from "./window.class";
import { ProviderRegistry, ScreenProviderInterface, WindowProviderInterface } from "@nut-tree/provider-interfaces";
import {
ElementInspectionProviderInterface,
LogProviderInterface,
ProviderRegistry,
ScreenProviderInterface,
WindowProviderInterface
} from "@nut-tree/provider-interfaces";
import { mockPartial } from "sneer";
import { Region } from "@nut-tree/shared";
import { Region, WindowElement, WindowElementDescription } from "@nut-tree/shared";
import { windowElementDescribedBy } from "../index";
import { NoopLogProvider } from "./provider/log/noop-log-provider.class";

describe("Window class", () => {
it("should retrieve the window region via provider", async () => {
Expand Down Expand Up @@ -54,4 +62,166 @@ describe("Window class", () => {
expect(windowMock).toHaveBeenCalledTimes(1);
expect(windowMock).toHaveBeenCalledWith(mockWindowHandle);
});

describe("element-inspection", () => {
it("should retrieve the window elements via provider", async () => {
// GIVEN
const elementInspectorMock = jest.fn();
const providerRegistryMock = mockPartial<ProviderRegistry>({
getWindowElementInspector(): ElementInspectionProviderInterface {
return mockPartial<ElementInspectionProviderInterface>({
getElements: elementInspectorMock
});
}
});
const mockWindowHandle = 123;
const maxElements = 1000;
const SUT = new Window(providerRegistryMock, mockWindowHandle);

// WHEN
await SUT.getElements(maxElements);

// THEN
expect(elementInspectorMock).toHaveBeenCalledTimes(1);
expect(elementInspectorMock).toHaveBeenCalledWith(mockWindowHandle, maxElements);
});

it("should search for window elements via provider", async () => {
// GIVEN
const elementInspectorMock = jest.fn();
const providerRegistryMock = mockPartial<ProviderRegistry>({
getWindowElementInspector(): ElementInspectionProviderInterface {
return mockPartial<ElementInspectionProviderInterface>({
findElement: elementInspectorMock,
findElements: elementInspectorMock
});
},
getLogProvider(): LogProviderInterface {
return new NoopLogProvider();
}
});
const mockWindowHandle = 123;
const description: WindowElementDescription = {
type: "test"
};
const SUT = new Window(providerRegistryMock, mockWindowHandle);

// WHEN
await SUT.find(windowElementDescribedBy(description));

// THEN
expect(elementInspectorMock).toHaveBeenCalledTimes(1);
expect(elementInspectorMock).toHaveBeenCalledWith(mockWindowHandle, description);
});

it("should search for multiple elements via provider", async () => {
// GIVEN
const elementInspectorMock = jest.fn();
const providerRegistryMock = mockPartial<ProviderRegistry>({
getWindowElementInspector(): ElementInspectionProviderInterface {
return mockPartial<ElementInspectionProviderInterface>({
findElement: elementInspectorMock,
findElements: elementInspectorMock
});
},
getLogProvider(): LogProviderInterface {
return new NoopLogProvider();
}
});
const mockWindowHandle = 123;
const description: WindowElementDescription = {
type: "test"
};
const SUT = new Window(providerRegistryMock, mockWindowHandle);

// WHEN
await SUT.findAll(windowElementDescribedBy(description));

// THEN
expect(elementInspectorMock).toHaveBeenCalledTimes(1);
expect(elementInspectorMock).toHaveBeenCalledWith(mockWindowHandle, description);
});

describe("hooks", () => {
it("should trigger registered hooks", async () => {
// GIVEN
const windowElementType = { type: "testElement" };
const windowElement = mockPartial<WindowElement>(windowElementType);
const hookMock = jest.fn();
const elementInspectorMock = jest.fn(() => Promise.resolve(windowElement));
const secondHookMock = jest.fn();
const providerRegistryMock = mockPartial<ProviderRegistry>({
getWindowElementInspector(): ElementInspectionProviderInterface {
return mockPartial<ElementInspectionProviderInterface>({
findElement: elementInspectorMock
});
},
getLogProvider(): LogProviderInterface {
return new NoopLogProvider();
}
});
const mockWindowHandle = 123;
const description: WindowElementDescription = {
type: "test"
};
const query = windowElementDescribedBy(description);
const SUT = new Window(providerRegistryMock, mockWindowHandle);
SUT.on(query, hookMock);
SUT.on(query, secondHookMock);

// WHEN
await SUT.find(query);

// THEN
expect(elementInspectorMock).toHaveBeenCalledTimes(1);
expect(elementInspectorMock).toHaveBeenCalledWith(mockWindowHandle, description);
expect(hookMock).toHaveBeenCalledTimes(1);
expect(hookMock).toHaveBeenCalledWith(windowElement);
expect(secondHookMock).toHaveBeenCalledTimes(1);
expect(secondHookMock).toHaveBeenCalledWith(windowElement);
});

it("should trigger registered hooks for all matches", async () => {
// GIVEN
const windowElementType = { type: "testElement" };
const secondElementType = { type: "secondElement" };
const windowElement = mockPartial<WindowElement>(windowElementType);
const secondElement = mockPartial<WindowElement>(secondElementType);
const mockMatches = [windowElement, secondElement];
const hookMock = jest.fn();
const elementInspectorMock = jest.fn(() => Promise.resolve(mockMatches));
const secondHookMock = jest.fn();
const providerRegistryMock = mockPartial<ProviderRegistry>({
getWindowElementInspector(): ElementInspectionProviderInterface {
return mockPartial<ElementInspectionProviderInterface>({
findElements: elementInspectorMock
});
},
getLogProvider(): LogProviderInterface {
return new NoopLogProvider();
}
});
const mockWindowHandle = 123;
const description: WindowElementDescription = {
type: "test"
};
const query = windowElementDescribedBy(description);
const SUT = new Window(providerRegistryMock, mockWindowHandle);
SUT.on(query, hookMock);
SUT.on(query, secondHookMock);

// WHEN
await SUT.findAll(query);

// THEN
expect(elementInspectorMock).toHaveBeenCalledTimes(1);
expect(elementInspectorMock).toHaveBeenCalledWith(mockWindowHandle, description);
expect(hookMock).toHaveBeenCalledTimes(mockMatches.length);
expect(hookMock).toHaveBeenCalledWith(windowElement);
expect(hookMock).toHaveBeenCalledWith(secondElement);
expect(secondHookMock).toHaveBeenCalledTimes(mockMatches.length);
expect(secondHookMock).toHaveBeenCalledWith(secondElement);
});
});
});
});
165 changes: 164 additions & 1 deletion core/nut.js/lib/window.class.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { Point, Region, Size, WindowInterface } from "@nut-tree/shared";
import {
FindHookCallback,
isWindowElementQuery,
OptionalSearchParameters,
Point,
Region,
Size,
WindowedFindInput,
WindowElement,
WindowElementCallback,
WindowElementQuery,
WindowElementResultFindInput,
WindowInterface
} from "@nut-tree/shared";
import { ProviderRegistry } from "@nut-tree/provider-interfaces";
import { timeout } from "./util/timeout.function";

export class Window implements WindowInterface {
private findHooks: Map<WindowElementQuery, WindowElementCallback[]>;

constructor(
private providerRegistry: ProviderRegistry,
private windowHandle: number
) {
this.findHooks = new Map<
WindowElementQuery,
WindowElementCallback[]
>();
}

get title(): Promise<string> {
Expand Down Expand Up @@ -65,4 +85,147 @@ export class Window implements WindowInterface {
async focus() {
return this.providerRegistry.getWindow().focusWindow(this.windowHandle);
}

async getElements(maxElements?: number): Promise<WindowElement> {
return this.providerRegistry.getWindowElementInspector().getElements(this.windowHandle, maxElements);
}

/**
* {@link find} will search for a single occurrence of a given search input in the current window.
* @param searchInput A {@link WindowedFindInput} instance
*/
public async find(
searchInput: WindowElementResultFindInput | Promise<WindowElementResultFindInput>
): Promise<WindowElement> {
const needle = await searchInput;
this.providerRegistry.getLogProvider().info(`Searching for ${needle} in window ${this.windowHandle}`);

try {
if (isWindowElementQuery(needle)) {
this.providerRegistry.getLogProvider().debug(`Running a window element search`);
const windowElement = await this.providerRegistry
.getWindowElementInspector()
.findElement(this.windowHandle, needle.by.description);
const possibleHooks = this.getHooksForInput(needle) || [];
this.providerRegistry
.getLogProvider()
.debug(`${possibleHooks.length} hooks triggered for match`);
for (const hook of possibleHooks) {
this.providerRegistry.getLogProvider().debug(`Executing hook`);
await hook(windowElement);
}
return windowElement;
}
throw new Error(
`Search input is not supported. Please use a valid search input type.`
);
} catch (e) {
const error = new Error(
`Searching for ${needle.id} failed. Reason: '${e}'`
);
this.providerRegistry.getLogProvider().error(error);
throw error;
}
}

/**
* {@link findAll} will search for multiple occurrence of a given search input in the current window.
* @param searchInput A {@link WindowedFindInput} instance
*/
public async findAll(
searchInput: WindowElementResultFindInput | Promise<WindowElementResultFindInput>
): Promise<WindowElement[]> {
// return this.providerRegistry.getWindowElementInspector().findElement(this.windowHandle, description);
const needle = await searchInput;
this.providerRegistry.getLogProvider().info(`Searching for ${needle} in window ${this.windowHandle}`);

try {
if (isWindowElementQuery(needle)) {
this.providerRegistry.getLogProvider().debug(`Running a window element search`);
const windowElements = await this.providerRegistry
.getWindowElementInspector()
.findElements(this.windowHandle, needle.by.description);
const possibleHooks = this.getHooksForInput(needle) || [];
this.providerRegistry
.getLogProvider()
.debug(`${possibleHooks.length} hooks triggered for match`);
for (const hook of possibleHooks) {
for (const windowElement of windowElements) {
this.providerRegistry.getLogProvider().debug(`Executing hook`);
await hook(windowElement);
}
}
return windowElements;
}
throw new Error(
`Search input is not supported. Please use a valid search input type.`
);
} catch (e) {
const error = new Error(
`Searching for ${needle.id} failed. Reason: '${e}'`
);
this.providerRegistry.getLogProvider().error(error);
throw error;
}
}

/**
* {@link waitFor} repeatedly searches for a query to appear in the window until it is found or the timeout is reached
* @param searchInput A {@link WindowElementQuery} instance
* @param timeoutMs Timeout in milliseconds after which {@link waitFor} fails
* @param updateInterval Update interval in milliseconds to retry search
* @param params {@link OptionalSearchParameters} which are used to fine tune search
*/
public async waitFor<PROVIDER_DATA_TYPE>(
searchInput: WindowElementQuery | Promise<WindowElementQuery>,
timeoutMs?: number,
updateInterval?: number,
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
): Promise<WindowElement> {
const needle = await searchInput;

const timeoutValue = timeoutMs ?? 5000;
const updateIntervalValue = updateInterval ?? 500;

this.providerRegistry
.getLogProvider()
.info(
`Waiting for ${needle.id} to appear in window. Timeout: ${
timeoutValue / 1000
} seconds, interval: ${updateIntervalValue} ms`
);
return timeout(
updateIntervalValue,
timeoutValue,
() => {
return this.find(needle);
},
{
signal: params?.abort
}
);
}

/**
* {@link on} registers a callback which is triggered once a certain searchInput image is found
* @param searchInput to trigger the callback on
* @param callback The {@link FindHookCallback} function to trigger
*/
public on(searchInput: WindowElementQuery, callback: WindowElementCallback): void {
const existingHooks = this.getHooksForInput(searchInput);
this.findHooks.set(searchInput, [...existingHooks, callback]);
this.providerRegistry
.getLogProvider()
.info(
`Registered callback for image ${searchInput.id}. There are currently ${
existingHooks.length + 1
} hooks registered`
);
}

private getHooksForInput(
input: WindowElementQuery
): WindowElementCallback[] {
return this.findHooks.get(input) ?? [];
}
}
11 changes: 10 additions & 1 deletion core/shared/lib/types/window.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Point, Region, Size } from "../objects";
import { OptionalSearchParameters, Point, Region, Size, WindowElementQuery } from "../objects";
import { WindowElement } from "./window-element.interface";
import { PointResultFindInput, RegionResultFindInput, WindowElementResultFindInput } from "./index";

Expand All @@ -22,6 +22,15 @@ export interface WindowInterface {
findAll(
searchInput: WindowElementResultFindInput | Promise<WindowElementResultFindInput>
): Promise<WindowElement[]>;

waitFor<PROVIDER_DATA_TYPE>(
searchInput: WindowElementQuery | Promise<WindowElementQuery>,
timeoutMs?: number,
updateInterval?: number,
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
): Promise<WindowElement>;

on(searchInput: WindowElementQuery, callback: WindowElementCallback): void;
}

export type WindowedFindInput =
Expand Down

0 comments on commit 1690171

Please sign in to comment.