Skip to content

Commit c619f02

Browse files
authored
Feature/455/non image find (#461)
* (#455) Made MatchRequest class generic to allow for generic needle types as well as additional generic provider data that gets passed to the provider implementation * (#455) BREAKING: Remove both searchMultipleScales and confidence from OptionalSearchParameters since these properties would be provider specific and should be handled via providerData * (#455) Handle generic provider data in ImageFinder interface * (#455) New query types to enable searching for things other than images * (#455) New interfaces to search for text or windows * (#455) Added new provider interfaces to registry, changed log level of register events to trace * (#455) Revert removing confidence property from OptionalSearchParameters * (#455) Extended find implementation conforms all existing tests * (#455) Increase coverage * (#455) Allow broader input on toShow jest matcher * (#455) Refactor handling of finder implementations, add tests * (#455) Added tests for different find queries * (#455) Added find input validation and tests * (#455) Query tests * (#455) Tests for DefaultProviderRegistry * (#455) Remove commented lines
1 parent 9a624d9 commit c619f02

22 files changed

+1571
-341
lines changed

Diff for: index.ts

+33
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { LineHelper } from "./lib/util/linehelper.class";
88
import { createWindowApi } from "./lib/window.function";
99
import providerRegistry from "./lib/provider/provider-registry.class";
1010
import { loadImageResource } from "./lib/imageResources.function";
11+
import { LineQuery, WindowQuery, WordQuery } from "./lib/query.class";
1112

1213
export {
1314
AssertClass,
@@ -70,6 +71,35 @@ const imageResource = (fileName: string) =>
7071
fileName
7172
);
7273

74+
const singleWord = (word: string | RegExp): WordQuery => {
75+
return {
76+
type: "text",
77+
id: `word-query-${word}`,
78+
by: {
79+
word,
80+
},
81+
};
82+
};
83+
const textLine = (line: string | RegExp): LineQuery => {
84+
return {
85+
type: "text",
86+
id: `line-query-${line}`,
87+
by: {
88+
line,
89+
},
90+
};
91+
};
92+
93+
const windowWithTitle = (title: string | RegExp): WindowQuery => {
94+
return {
95+
type: "window",
96+
id: `window-by-title-query-${title}`,
97+
by: {
98+
title,
99+
},
100+
};
101+
};
102+
73103
export { fetchFromUrl } from "./lib/imageResources.function";
74104

75105
export {
@@ -88,4 +118,7 @@ export {
88118
loadImage,
89119
saveImage,
90120
imageResource,
121+
singleWord,
122+
textLine,
123+
windowWithTitle,
91124
};

Diff for: lib/assert.class.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("Assert", () => {
1515
// GIVEN
1616
ScreenClass.prototype.find = jest.fn(() =>
1717
Promise.resolve(new Region(0, 0, 100, 100))
18-
);
18+
) as any;
1919
const screenMock = new ScreenClass(providerRegistry);
2020
const SUT = new AssertClass(screenMock);
2121
const needle = mockPartial<Image>({
@@ -30,7 +30,7 @@ describe("Assert", () => {
3030

3131
it("isVisible should throw if a match is found.", async () => {
3232
// GIVEN
33-
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
33+
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")) as any;
3434
const screenMock = new ScreenClass(providerRegistry);
3535
const SUT = new AssertClass(screenMock);
3636
const needle = mockPartial<Image>({
@@ -47,7 +47,7 @@ describe("Assert", () => {
4747

4848
it("isVisible should throw if a match is found.", async () => {
4949
// GIVEN
50-
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
50+
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")) as any;
5151
const screenMock = new ScreenClass(providerRegistry);
5252
const SUT = new AssertClass(screenMock);
5353
const searchRegion = new Region(10, 10, 10, 10);
@@ -67,7 +67,7 @@ describe("Assert", () => {
6767
// GIVEN
6868
ScreenClass.prototype.find = jest.fn(() =>
6969
Promise.resolve(new Region(0, 0, 100, 100))
70-
);
70+
) as any;
7171
const screenMock = new ScreenClass(providerRegistry);
7272
const SUT = new AssertClass(screenMock);
7373
const needle = mockPartial<Image>({
@@ -84,7 +84,7 @@ describe("Assert", () => {
8484

8585
it("isVisible should throw if a match is found.", async () => {
8686
// GIVEN
87-
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo"));
87+
ScreenClass.prototype.find = jest.fn(() => Promise.reject("foo")) as any;
8888
const screenMock = new ScreenClass(providerRegistry);
8989
const SUT = new AssertClass(screenMock);
9090
const needle = mockPartial<Image>({

Diff for: lib/assert.class.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { Region } from "./region.class";
2-
import { ScreenClass } from "./screen.class";
3-
import { FirstArgumentType } from "./typings";
2+
import { FindInput, ScreenClass } from "./screen.class";
43
import { OptionalSearchParameters } from "./optionalsearchparameters.class";
54

65
export class AssertClass {
76
constructor(private screen: ScreenClass) {}
87

98
public async isVisible(
10-
needle: FirstArgumentType<typeof ScreenClass.prototype.find>,
11-
searchRegion?: Region,
9+
searchInput: FindInput | Promise<FindInput>,
10+
searchRegion?: Region | Promise<Region>,
1211
confidence?: number
13-
) {
14-
const identifier = (await needle).id;
12+
): Promise<void> {
13+
const needle = await searchInput;
14+
const identifier = needle.id;
1515

1616
try {
1717
await this.screen.find(needle, {
1818
searchRegion,
1919
confidence,
20-
} as OptionalSearchParameters);
20+
} as OptionalSearchParameters<never>);
2121
} catch (err) {
2222
if (searchRegion !== undefined) {
2323
throw new Error(
@@ -30,17 +30,18 @@ export class AssertClass {
3030
}
3131

3232
public async notVisible(
33-
needle: FirstArgumentType<typeof ScreenClass.prototype.find>,
34-
searchRegion?: Region,
33+
searchInput: FindInput | Promise<FindInput>,
34+
searchRegion?: Region | Promise<Region>,
3535
confidence?: number
3636
) {
37-
const identifier = (await needle).id;
37+
const needle = await searchInput;
38+
const identifier = needle.id;
3839

3940
try {
4041
await this.screen.find(needle, {
4142
searchRegion,
4243
confidence,
43-
} as OptionalSearchParameters);
44+
} as OptionalSearchParameters<never>);
4445
} catch (err) {
4546
return;
4647
}

Diff for: lib/expect/jest.matcher.function.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,14 @@ import { Region } from "../region.class";
33
import { toBeAt } from "./matchers/toBeAt.function";
44
import { toBeIn } from "./matchers/toBeIn.function";
55
import { toShow } from "./matchers/toShow.function";
6-
import { FirstArgumentType } from "../typings";
7-
import { ScreenClass } from "../screen.class";
6+
import { FindInput } from "../screen.class";
87

98
declare global {
109
namespace jest {
1110
interface Matchers<R> {
1211
toBeAt: (position: Point) => {};
1312
toBeIn: (region: Region) => {};
14-
toShow: (
15-
needle: FirstArgumentType<typeof ScreenClass.prototype.find>,
16-
confidence?: number
17-
) => {};
13+
toShow: (needle: FindInput, confidence?: number) => {};
1814
}
1915
}
2016
}

Diff for: lib/expect/matchers/toShow.function.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { ScreenClass } from "../../screen.class";
2-
import { FirstArgumentType } from "../../typings";
1+
import { FindInput, ScreenClass } from "../../screen.class";
32
import { OptionalSearchParameters } from "../../optionalsearchparameters.class";
43

54
export const toShow = async (
65
received: ScreenClass,
7-
needle: FirstArgumentType<typeof ScreenClass.prototype.find>,
6+
needle: FindInput,
87
confidence?: number
98
) => {
109
let locationParams;

Diff for: lib/match-request.class.spec.ts

+70-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,78 @@
11
import { Image } from "./image.class";
2-
import { MatchRequest } from "./match-request.class";
2+
import {
3+
createMatchRequest,
4+
isImageMatchRequest,
5+
isTextMatchRequest,
6+
} from "./match-request.class";
7+
import { mockPartial } from "sneer";
8+
import { ProviderRegistry } from "./provider/provider-registry.class";
9+
import { LogProviderInterface } from "./provider";
10+
import { NoopLogProvider } from "./provider/log/noop-log-provider.class";
11+
import { Region } from "./region.class";
12+
import { TextQuery } from "./query.class";
313

4-
jest.mock("jimp", () => {});
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
const providerRegistryMock = mockPartial<ProviderRegistry>({
19+
getLogProvider(): LogProviderInterface {
20+
return new NoopLogProvider();
21+
},
22+
});
23+
24+
const screenImage = new Image(0, 0, Buffer.of(0), 3, "dummy", 0, 0);
25+
const dummyRegion = new Region(0, 0, 0, 0);
526

627
describe("MatchRequest", () => {
7-
it("should default to multi-scale matching", () => {
8-
const SUT = new MatchRequest(
9-
new Image(100, 100, Buffer.from([]), 3, "haystack_image", 4, 100 * 4),
10-
new Image(100, 100, Buffer.from([]), 3, "needle_image", 4, 100 * 4),
11-
0.99
28+
it("should create an image match request for images", () => {
29+
// GIVEN
30+
const needle = new Image(0, 0, Buffer.of(0), 3, "dummy", 0, 0);
31+
32+
// WHEN
33+
const matchRequest = createMatchRequest(
34+
providerRegistryMock,
35+
needle,
36+
dummyRegion,
37+
0,
38+
screenImage
1239
);
1340

14-
expect(SUT.searchMultipleScales).toBeTruthy();
41+
// THEN
42+
expect(isImageMatchRequest(matchRequest)).toBeTruthy();
1543
});
44+
45+
it.each<TextQuery>([
46+
{
47+
id: "dummy",
48+
type: "text",
49+
by: {
50+
word: "dummy-query",
51+
},
52+
},
53+
{
54+
id: "dummy",
55+
type: "text",
56+
by: {
57+
line: "dummy-query",
58+
},
59+
},
60+
])(
61+
"should create a text match request for text queries",
62+
(needle: TextQuery) => {
63+
// GIVEN
64+
65+
// WHEN
66+
const matchRequest = createMatchRequest(
67+
providerRegistryMock,
68+
needle,
69+
dummyRegion,
70+
0,
71+
screenImage
72+
);
73+
74+
// THEN
75+
expect(isTextMatchRequest(matchRequest)).toBeTruthy();
76+
}
77+
);
1678
});

Diff for: lib/match-request.class.ts

+90-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,95 @@
1-
import { Image } from "./image.class";
1+
import { Image, isImage } from "./image.class";
2+
import { Region } from "./region.class";
3+
import { OptionalSearchParameters } from "./optionalsearchparameters.class";
4+
import { isLineQuery, isTextQuery, TextQuery } from "./query.class";
5+
import { RegionResultFindInput } from "./screen.class";
6+
import { ProviderRegistry } from "./provider/provider-registry.class";
27

3-
export class MatchRequest {
4-
constructor(
8+
export class MatchRequest<NEEDLE_TYPE, PROVIDER_DATA_TYPE> {
9+
public constructor(
510
public readonly haystack: Image,
6-
public readonly needle: Image,
11+
public readonly needle: NEEDLE_TYPE,
712
public readonly confidence: number,
8-
public readonly searchMultipleScales: boolean = true
13+
public readonly providerData?: PROVIDER_DATA_TYPE
914
) {}
1015
}
16+
17+
export function isImageMatchRequest<PROVIDER_DATA_TYPE>(
18+
matchRequest: any
19+
): matchRequest is MatchRequest<Image, PROVIDER_DATA_TYPE> {
20+
return isImage(matchRequest.needle);
21+
}
22+
23+
export function isTextMatchRequest<PROVIDER_DATA_TYPE>(
24+
matchRequest: any
25+
): matchRequest is MatchRequest<TextQuery, PROVIDER_DATA_TYPE> {
26+
return isTextQuery(matchRequest.needle);
27+
}
28+
29+
export function createMatchRequest<PROVIDER_DATA_TYPE>(
30+
providerRegistry: ProviderRegistry,
31+
needle: TextQuery | Promise<TextQuery>,
32+
searchRegion: Region,
33+
minMatch: number,
34+
screenImage: Image,
35+
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
36+
): MatchRequest<TextQuery, PROVIDER_DATA_TYPE>;
37+
export function createMatchRequest<PROVIDER_DATA_TYPE>(
38+
providerRegistry: ProviderRegistry,
39+
needle: Image | Promise<Image>,
40+
searchRegion: Region,
41+
minMatch: number,
42+
screenImage: Image,
43+
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
44+
): MatchRequest<Image, PROVIDER_DATA_TYPE>;
45+
export function createMatchRequest<PROVIDER_DATA_TYPE>(
46+
providerRegistry: ProviderRegistry,
47+
needle: RegionResultFindInput | Promise<RegionResultFindInput>,
48+
searchRegion: Region,
49+
minMatch: number,
50+
screenImage: Image,
51+
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
52+
):
53+
| MatchRequest<TextQuery, PROVIDER_DATA_TYPE>
54+
| MatchRequest<Image, PROVIDER_DATA_TYPE>;
55+
export function createMatchRequest<PROVIDER_DATA_TYPE>(
56+
providerRegistry: ProviderRegistry,
57+
needle: RegionResultFindInput | Promise<RegionResultFindInput>,
58+
searchRegion: Region,
59+
minMatch: number,
60+
screenImage: Image,
61+
params?: OptionalSearchParameters<PROVIDER_DATA_TYPE>
62+
):
63+
| MatchRequest<Image, PROVIDER_DATA_TYPE>
64+
| MatchRequest<TextQuery, PROVIDER_DATA_TYPE> {
65+
if (isImage(needle)) {
66+
providerRegistry
67+
.getLogProvider()
68+
.info(
69+
`Searching for image ${
70+
needle.id
71+
} in region ${searchRegion.toString()}. Required confidence: ${minMatch}`
72+
);
73+
74+
return new MatchRequest(
75+
screenImage,
76+
needle,
77+
minMatch,
78+
params?.providerData
79+
);
80+
} else if (isTextQuery(needle)) {
81+
providerRegistry.getLogProvider().info(
82+
`Searching for ${isLineQuery(needle) ? "line" : "word"} {
83+
${isLineQuery(needle) ? needle.by.line : needle.by.word}
84+
} in region ${searchRegion.toString()}. Required confidence: ${minMatch}`
85+
);
86+
87+
return new MatchRequest(
88+
screenImage,
89+
needle,
90+
minMatch,
91+
params?.providerData
92+
);
93+
}
94+
throw new Error(`Unknown input type: ${JSON.stringify(needle)}`);
95+
}

0 commit comments

Comments
 (0)