Skip to content

Commit 8437da1

Browse files
authored
Feature/351/runtime typeguards (#352)
* (#351) Added typeguards for Point, Region and Image * (#351) Missed Screen class, fixed docstring for keyboard
1 parent b02abe6 commit 8437da1

14 files changed

+409
-60
lines changed

lib/image.class.spec.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Image} from "./image.class";
1+
import {Image, isImage} from "./image.class";
22
import {imageToJimp} from "./provider/io/imageToJimp.function";
33
import {ColorMode} from "./colormode.enum";
44

@@ -65,4 +65,65 @@ describe("Image class", () => {
6565
expect(imageToJimp).not.toBeCalledTimes(1)
6666
});
6767
});
68+
69+
describe('isImage typeguard', () => {
70+
it('should identify an Image', () => {
71+
// GIVEN
72+
const img = new Image(100, 100, Buffer.from([]), 4, 'foo');
73+
74+
// WHEN
75+
const result = isImage(img);
76+
77+
// THEN
78+
expect(result).toBeTruthy();
79+
});
80+
81+
it('should rule out non-objects', () => {
82+
// GIVEN
83+
const i = "foo";
84+
85+
// WHEN
86+
const result = isImage(i);
87+
88+
// THEN
89+
expect(result).toBeFalsy();
90+
});
91+
92+
it('should rule out possible object with missing properties', () => {
93+
// GIVEN
94+
const img = {
95+
width: 100,
96+
height: 100,
97+
data: Buffer.from([]),
98+
channels: 'foo',
99+
id: 'foo',
100+
colorMode: ColorMode.BGR
101+
};
102+
103+
// WHEN
104+
const result = isImage(img);
105+
106+
// THEN
107+
expect(result).toBeFalsy();
108+
});
109+
110+
it('should rule out possible object with wrong property type', () => {
111+
// GIVEN
112+
const img = {
113+
width: 100,
114+
height: 100,
115+
data: Buffer.from([]),
116+
channels: 'foo',
117+
id: 'foo',
118+
colorMode: ColorMode.BGR,
119+
pixelDensity: 25
120+
};
121+
122+
// WHEN
123+
const result = isImage(img);
124+
125+
// THEN
126+
expect(result).toBeFalsy();
127+
});
128+
})
68129
});

lib/image.class.ts

+20
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,23 @@ export class Image {
7070
return new Image(width, height, jimpImage.bitmap.data, channels, id);
7171
}
7272
}
73+
74+
const testImage = new Image(100, 100, Buffer.from([]), 4, "typeCheck");
75+
const imageKeys = Object.keys(testImage);
76+
77+
export function isImage(possibleImage: any): possibleImage is Image {
78+
if (typeof possibleImage !== 'object') {
79+
return false;
80+
}
81+
for (const key of imageKeys) {
82+
if (!(key in possibleImage)) {
83+
return false;
84+
}
85+
const possibleImageKeyType = typeof possibleImage[key];
86+
const imageKeyType = typeof testImage[key as keyof typeof testImage];
87+
if (possibleImageKeyType !== imageKeyType) {
88+
return false
89+
}
90+
}
91+
return true;
92+
}

lib/keyboard.class.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class KeyboardClass {
6565
* @example
6666
* ```typescript
6767
* // Will press and hold key combination STRG + V
68-
* await keyboard.pressKey(Key.STRG, Key.A);
68+
* await keyboard.pressKey(Key.STRG, Key.V);
6969
* ```
7070
*
7171
* @param keys Array of {@link Key}s to press and hold
@@ -87,7 +87,7 @@ export class KeyboardClass {
8787
* @example
8888
* ```typescript
8989
* // Will release key combination STRG + V
90-
* await keyboard.releaseKey(Key.STRG, Key.A);
90+
* await keyboard.releaseKey(Key.STRG, Key.V);
9191
* ```
9292
*
9393
* @param keys Array of {@link Key}s to release

lib/location.function.spec.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
1-
import { centerOf, randomPointIn } from "./location.function";
2-
import { Point } from "./point.class";
3-
import { Region } from "./region.class";
1+
import {centerOf, randomPointIn} from "./location.function";
2+
import {Point} from "./point.class";
3+
import {Region} from "./region.class";
44

55
describe("Location", () => {
6-
it("should return the center point of an area.", () => {
7-
const expected = new Point(2, 2);
8-
const testRegion = new Region(0, 0, 4, 4);
6+
describe('centerOf', () => {
7+
it("should return the center point of an area.", () => {
8+
const expected = new Point(2, 2);
9+
const testRegion = new Region(0, 0, 4, 4);
910

10-
expect(centerOf(testRegion)).resolves.toEqual(expected);
11-
});
11+
expect(centerOf(testRegion)).resolves.toEqual(expected);
12+
});
1213

13-
it("should return a random point inside of an area.", async () => {
14-
const testRegion = new Region(100, 20, 50, 35);
15-
const result = await randomPointIn(testRegion);
16-
expect(result.x).toBeGreaterThanOrEqual(testRegion.left);
17-
expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width);
18-
expect(result.y).toBeGreaterThanOrEqual(testRegion.top);
19-
expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height);
20-
});
14+
it("should throw on non Region input", async () => {
15+
const testRegion = {
16+
left: 0,
17+
top: 0,
18+
width: 4
19+
};
20+
21+
await expect(centerOf(testRegion as Region)).rejects.toThrowError(/^centerOf requires a Region, but received/);
22+
});
23+
});
24+
25+
describe('randomPointIn', () => {
26+
it("should return a random point inside of an area.", async () => {
27+
const testRegion = new Region(100, 20, 50, 35);
28+
const result = await randomPointIn(testRegion);
29+
expect(result.x).toBeGreaterThanOrEqual(testRegion.left);
30+
expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width);
31+
expect(result.y).toBeGreaterThanOrEqual(testRegion.top);
32+
expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height);
33+
});
34+
35+
it("should throw on non Region input", async () => {
36+
const testRegion = {
37+
left: 0,
38+
top: 0,
39+
width: 4
40+
};
41+
42+
await expect(randomPointIn(testRegion as Region)).rejects.toThrowError(/^randomPointIn requires a Region, but received/);
43+
});
44+
});
2145
});

lib/location.function.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
import { Point } from "./point.class";
2-
import { Region } from "./region.class";
1+
import {Point} from "./point.class";
2+
import {isRegion, Region} from "./region.class";
33

44
/**
55
* {@link centerOf} returns the center {@link Point} for a given {@link Region}
66
* @param target {@link Region} to determine the center {@link Point} for
77
*/
88
export const centerOf = async (target: Region | Promise<Region>): Promise<Point> => {
9-
const targetRegion = await target;
10-
const x = Math.floor(targetRegion.left + targetRegion.width / 2);
11-
const y = Math.floor(targetRegion.top + targetRegion.height / 2);
9+
const targetRegion = await target;
10+
if (!isRegion(targetRegion)) {
11+
throw Error(`centerOf requires a Region, but received ${JSON.stringify(targetRegion)}`)
12+
}
13+
const x = Math.floor(targetRegion.left + targetRegion.width / 2);
14+
const y = Math.floor(targetRegion.top + targetRegion.height / 2);
1215

13-
return new Point(x, y);
16+
return new Point(x, y);
1417
};
1518

1619
/**
1720
* {@link randomPointIn} returns a random {@link Point} within a given {@link Region}
1821
* @param target {@link Region} the random {@link Point} has to be within
1922
*/
2023
export const randomPointIn = async (target: Region | Promise<Region>): Promise<Point> => {
21-
const targetRegion = await target;
22-
const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width);
23-
const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height);
24+
const targetRegion = await target;
25+
if (!isRegion(targetRegion)) {
26+
throw Error(`randomPointIn requires a Region, but received ${JSON.stringify(targetRegion)}`)
27+
}
28+
const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width);
29+
const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height);
2430

25-
return new Point(x, y);
31+
return new Point(x, y);
2632
};

lib/mouse.class.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Button} from "./button.enum";
2-
import {Point} from "./point.class";
2+
import {isPoint, Point} from "./point.class";
33
import {busyWaitForNanoSeconds, sleep} from "./sleep.function";
44
import {calculateMovementTimesteps, EasingFunction, linear} from "./mouse-movement.function";
55
import {ProviderRegistry} from "./provider/provider-registry.class";
@@ -36,6 +36,9 @@ export class MouseClass {
3636
* @param target {@link Point} to move the cursor to
3737
*/
3838
public async setPosition(target: Point): Promise<MouseClass> {
39+
if (!isPoint(target)) {
40+
throw Error(`setPosition requires a Point, but received ${JSON.stringify(target)}`)
41+
}
3942
return new Promise<MouseClass>(async (resolve, reject) => {
4043
try {
4144
await this.providerRegistry.getMouse().setMousePosition(target);
@@ -67,7 +70,7 @@ export class MouseClass {
6770
const node = pathSteps[idx];
6871
const minTime = timeSteps[idx];
6972
await busyWaitForNanoSeconds(minTime);
70-
await this.providerRegistry.getMouse().setMousePosition(node);
73+
await this.setPosition(node);
7174
}
7275
resolve(this);
7376
} catch (e) {

lib/movement.function.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {MovementApi} from "./movement-api.interface";
2-
import {Point} from "./point.class";
2+
import {isPoint, Point} from "./point.class";
33
import {LineHelper} from "./util/linehelper.class";
44
import {ProviderRegistry} from "./provider/provider-registry.class";
55

@@ -19,6 +19,9 @@ export const createMovementApi = (providerRegistry: ProviderRegistry, lineHelper
1919
},
2020
straightTo: async (target: Point | Promise<Point>): Promise<Point[]> => {
2121
const targetPoint = await target;
22+
if (!isPoint(targetPoint)) {
23+
throw Error(`straightTo requires a Point, but received ${JSON.stringify(targetPoint)}`)
24+
}
2225
const origin = await providerRegistry.getMouse().currentMousePosition();
2326
return lineHelper.straightLine(origin, targetPoint);
2427
},

lib/point.class.spec.ts

+57-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,61 @@
1-
import { Point } from "./point.class";
1+
import {isPoint, Point} from "./point.class";
22

33
describe("Point", () => {
4-
it("should return a proper string representation.", () => {
5-
const point = new Point(10, 15);
6-
const expected = "(10, 15)";
4+
it("should return a proper string representation.", () => {
5+
const point = new Point(10, 15);
6+
const expected = "(10, 15)";
77

8-
expect(point.toString()).toEqual(expected);
9-
});
8+
expect(point.toString()).toEqual(expected);
9+
});
10+
11+
describe('isPoint typeguard', () => {
12+
it('should identify a Point', () => {
13+
// GIVEN
14+
const p = new Point(100, 100);
15+
16+
// WHEN
17+
const result = isPoint(p);
18+
19+
// THEN
20+
expect(result).toBeTruthy();
21+
});
22+
23+
it('should rule out non-objects', () => {
24+
// GIVEN
25+
const p = "foo";
26+
27+
// WHEN
28+
const result = isPoint(p);
29+
30+
// THEN
31+
expect(result).toBeFalsy();
32+
});
33+
34+
it('should rule out possible object with missing properties', () => {
35+
// GIVEN
36+
const p = {
37+
x: 100
38+
};
39+
40+
// WHEN
41+
const result = isPoint(p);
42+
43+
// THEN
44+
expect(result).toBeFalsy();
45+
});
46+
47+
it('should rule out possible object with wrong property type', () => {
48+
// GIVEN
49+
const p = {
50+
x: 100,
51+
y: 'foo'
52+
};
53+
54+
// WHEN
55+
const result = isPoint(p);
56+
57+
// THEN
58+
expect(result).toBeFalsy();
59+
});
60+
})
1061
});

lib/point.class.ts

+20
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,23 @@ export class Point {
55
return `(${this.x}, ${this.y})`;
66
}
77
}
8+
9+
const testPoint = new Point(100, 100);
10+
const pointKeys = Object.keys(testPoint);
11+
12+
export function isPoint(possiblePoint: any): possiblePoint is Point {
13+
if (typeof possiblePoint !== 'object') {
14+
return false;
15+
}
16+
for (const key of pointKeys) {
17+
if (!(key in possiblePoint)) {
18+
return false;
19+
}
20+
const possiblePointKeyType = typeof possiblePoint[key];
21+
const pointKeyType = typeof testPoint[key as keyof typeof testPoint];
22+
if (possiblePointKeyType!== pointKeyType) {
23+
return false
24+
}
25+
}
26+
return true;
27+
}

0 commit comments

Comments
 (0)