Skip to content

Feature/351/runtime typeguards #352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion lib/image.class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Image} from "./image.class";
import {Image, isImage} from "./image.class";
import {imageToJimp} from "./provider/io/imageToJimp.function";
import {ColorMode} from "./colormode.enum";

Expand Down Expand Up @@ -65,4 +65,65 @@ describe("Image class", () => {
expect(imageToJimp).not.toBeCalledTimes(1)
});
});

describe('isImage typeguard', () => {
it('should identify an Image', () => {
// GIVEN
const img = new Image(100, 100, Buffer.from([]), 4, 'foo');

// WHEN
const result = isImage(img);

// THEN
expect(result).toBeTruthy();
});

it('should rule out non-objects', () => {
// GIVEN
const i = "foo";

// WHEN
const result = isImage(i);

// THEN
expect(result).toBeFalsy();
});

it('should rule out possible object with missing properties', () => {
// GIVEN
const img = {
width: 100,
height: 100,
data: Buffer.from([]),
channels: 'foo',
id: 'foo',
colorMode: ColorMode.BGR
};

// WHEN
const result = isImage(img);

// THEN
expect(result).toBeFalsy();
});

it('should rule out possible object with wrong property type', () => {
// GIVEN
const img = {
width: 100,
height: 100,
data: Buffer.from([]),
channels: 'foo',
id: 'foo',
colorMode: ColorMode.BGR,
pixelDensity: 25
};

// WHEN
const result = isImage(img);

// THEN
expect(result).toBeFalsy();
});
})
});
20 changes: 20 additions & 0 deletions lib/image.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ export class Image {
return new Image(width, height, jimpImage.bitmap.data, channels, id);
}
}

const testImage = new Image(100, 100, Buffer.from([]), 4, "typeCheck");
const imageKeys = Object.keys(testImage);

export function isImage(possibleImage: any): possibleImage is Image {
if (typeof possibleImage !== 'object') {
return false;
}
for (const key of imageKeys) {
if (!(key in possibleImage)) {
return false;
}
const possibleImageKeyType = typeof possibleImage[key];
const imageKeyType = typeof testImage[key as keyof typeof testImage];
if (possibleImageKeyType !== imageKeyType) {
return false
}
}
return true;
}
4 changes: 2 additions & 2 deletions lib/keyboard.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class KeyboardClass {
* @example
* ```typescript
* // Will press and hold key combination STRG + V
* await keyboard.pressKey(Key.STRG, Key.A);
* await keyboard.pressKey(Key.STRG, Key.V);
* ```
*
* @param keys Array of {@link Key}s to press and hold
Expand All @@ -87,7 +87,7 @@ export class KeyboardClass {
* @example
* ```typescript
* // Will release key combination STRG + V
* await keyboard.releaseKey(Key.STRG, Key.A);
* await keyboard.releaseKey(Key.STRG, Key.V);
* ```
*
* @param keys Array of {@link Key}s to release
Expand Down
56 changes: 40 additions & 16 deletions lib/location.function.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
import { centerOf, randomPointIn } from "./location.function";
import { Point } from "./point.class";
import { Region } from "./region.class";
import {centerOf, randomPointIn} from "./location.function";
import {Point} from "./point.class";
import {Region} from "./region.class";

describe("Location", () => {
it("should return the center point of an area.", () => {
const expected = new Point(2, 2);
const testRegion = new Region(0, 0, 4, 4);
describe('centerOf', () => {
it("should return the center point of an area.", () => {
const expected = new Point(2, 2);
const testRegion = new Region(0, 0, 4, 4);

expect(centerOf(testRegion)).resolves.toEqual(expected);
});
expect(centerOf(testRegion)).resolves.toEqual(expected);
});

it("should return a random point inside of an area.", async () => {
const testRegion = new Region(100, 20, 50, 35);
const result = await randomPointIn(testRegion);
expect(result.x).toBeGreaterThanOrEqual(testRegion.left);
expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width);
expect(result.y).toBeGreaterThanOrEqual(testRegion.top);
expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height);
});
it("should throw on non Region input", async () => {
const testRegion = {
left: 0,
top: 0,
width: 4
};

await expect(centerOf(testRegion as Region)).rejects.toThrowError(/^centerOf requires a Region, but received/);
});
});

describe('randomPointIn', () => {
it("should return a random point inside of an area.", async () => {
const testRegion = new Region(100, 20, 50, 35);
const result = await randomPointIn(testRegion);
expect(result.x).toBeGreaterThanOrEqual(testRegion.left);
expect(result.x).toBeLessThanOrEqual(testRegion.left + testRegion.width);
expect(result.y).toBeGreaterThanOrEqual(testRegion.top);
expect(result.y).toBeLessThanOrEqual(testRegion.top + testRegion.height);
});

it("should throw on non Region input", async () => {
const testRegion = {
left: 0,
top: 0,
width: 4
};

await expect(randomPointIn(testRegion as Region)).rejects.toThrowError(/^randomPointIn requires a Region, but received/);
});
});
});
26 changes: 16 additions & 10 deletions lib/location.function.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { Point } from "./point.class";
import { Region } from "./region.class";
import {Point} from "./point.class";
import {isRegion, Region} from "./region.class";

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

return new Point(x, y);
return new Point(x, y);
};

/**
* {@link randomPointIn} returns a random {@link Point} within a given {@link Region}
* @param target {@link Region} the random {@link Point} has to be within
*/
export const randomPointIn = async (target: Region | Promise<Region>): Promise<Point> => {
const targetRegion = await target;
const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width);
const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height);
const targetRegion = await target;
if (!isRegion(targetRegion)) {
throw Error(`randomPointIn requires a Region, but received ${JSON.stringify(targetRegion)}`)
}
const x = Math.floor(targetRegion.left + Math.random() * targetRegion.width);
const y = Math.floor(targetRegion.top + Math.random() * targetRegion.height);

return new Point(x, y);
return new Point(x, y);
};
7 changes: 5 additions & 2 deletions lib/mouse.class.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Button} from "./button.enum";
import {Point} from "./point.class";
import {isPoint, Point} from "./point.class";
import {busyWaitForNanoSeconds, sleep} from "./sleep.function";
import {calculateMovementTimesteps, EasingFunction, linear} from "./mouse-movement.function";
import {ProviderRegistry} from "./provider/provider-registry.class";
Expand Down Expand Up @@ -36,6 +36,9 @@ export class MouseClass {
* @param target {@link Point} to move the cursor to
*/
public async setPosition(target: Point): Promise<MouseClass> {
if (!isPoint(target)) {
throw Error(`setPosition requires a Point, but received ${JSON.stringify(target)}`)
}
return new Promise<MouseClass>(async (resolve, reject) => {
try {
await this.providerRegistry.getMouse().setMousePosition(target);
Expand Down Expand Up @@ -67,7 +70,7 @@ export class MouseClass {
const node = pathSteps[idx];
const minTime = timeSteps[idx];
await busyWaitForNanoSeconds(minTime);
await this.providerRegistry.getMouse().setMousePosition(node);
await this.setPosition(node);
}
resolve(this);
} catch (e) {
Expand Down
5 changes: 4 additions & 1 deletion lib/movement.function.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {MovementApi} from "./movement-api.interface";
import {Point} from "./point.class";
import {isPoint, Point} from "./point.class";
import {LineHelper} from "./util/linehelper.class";
import {ProviderRegistry} from "./provider/provider-registry.class";

Expand All @@ -19,6 +19,9 @@ export const createMovementApi = (providerRegistry: ProviderRegistry, lineHelper
},
straightTo: async (target: Point | Promise<Point>): Promise<Point[]> => {
const targetPoint = await target;
if (!isPoint(targetPoint)) {
throw Error(`straightTo requires a Point, but received ${JSON.stringify(targetPoint)}`)
}
const origin = await providerRegistry.getMouse().currentMousePosition();
return lineHelper.straightLine(origin, targetPoint);
},
Expand Down
63 changes: 57 additions & 6 deletions lib/point.class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,61 @@
import { Point } from "./point.class";
import {isPoint, Point} from "./point.class";

describe("Point", () => {
it("should return a proper string representation.", () => {
const point = new Point(10, 15);
const expected = "(10, 15)";
it("should return a proper string representation.", () => {
const point = new Point(10, 15);
const expected = "(10, 15)";

expect(point.toString()).toEqual(expected);
});
expect(point.toString()).toEqual(expected);
});

describe('isPoint typeguard', () => {
it('should identify a Point', () => {
// GIVEN
const p = new Point(100, 100);

// WHEN
const result = isPoint(p);

// THEN
expect(result).toBeTruthy();
});

it('should rule out non-objects', () => {
// GIVEN
const p = "foo";

// WHEN
const result = isPoint(p);

// THEN
expect(result).toBeFalsy();
});

it('should rule out possible object with missing properties', () => {
// GIVEN
const p = {
x: 100
};

// WHEN
const result = isPoint(p);

// THEN
expect(result).toBeFalsy();
});

it('should rule out possible object with wrong property type', () => {
// GIVEN
const p = {
x: 100,
y: 'foo'
};

// WHEN
const result = isPoint(p);

// THEN
expect(result).toBeFalsy();
});
})
});
20 changes: 20 additions & 0 deletions lib/point.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,23 @@ export class Point {
return `(${this.x}, ${this.y})`;
}
}

const testPoint = new Point(100, 100);
const pointKeys = Object.keys(testPoint);

export function isPoint(possiblePoint: any): possiblePoint is Point {
if (typeof possiblePoint !== 'object') {
return false;
}
for (const key of pointKeys) {
if (!(key in possiblePoint)) {
return false;
}
const possiblePointKeyType = typeof possiblePoint[key];
const pointKeyType = typeof testPoint[key as keyof typeof testPoint];
if (possiblePointKeyType!== pointKeyType) {
return false
}
}
return true;
}
Loading