Skip to content

Commit 1ec9d62

Browse files
authored
Feature/307/default imagereader imagewriter (#308)
* (#307) Add jimp dependencz * (#307) Implement image reader * (#307) Implement image writer * (#307) Test data * (#307) Register both providers * (#307) Stick to BGR color format when loading images * (#307) Expose imagereader and imagewriter through utility functions * (#307) Updated tests to verify calls to Jimp * (#307) Mock jimp in testcases using the pluginRegistry to avoid ReferenceErrors * (#307) Mock jimp in e2e testcases to avoid ReferenceErrors
1 parent ab84e1d commit 1ec9d62

21 files changed

+752
-0
lines changed

Diff for: e2e/plugin-test/keyboard.class.e2e.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { jestMatchers, Key, keyboard, screen } from "@nut-tree/nut-js";
22
import "@nut-tree/template-matcher";
33

4+
jest.mock('jimp', () => {});
5+
46
jest.setTimeout(30000);
57
expect.extend(jestMatchers);
68

Diff for: index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ const assert = new AssertClass(screen);
5050
const {straightTo, up, down, left, right} = createMovementApi(nativeActions, lineHelper);
5151
const {getWindows, getActiveWindow} = createWindowApi(nativeActions);
5252

53+
const loadImage = providerRegistry.getImageReader().load;
54+
const saveImage = providerRegistry.getImageWriter().store;
55+
5356
export {
5457
clipboard,
5558
keyboard,
@@ -63,4 +66,6 @@ export {
6366
right,
6467
getWindows,
6568
getActiveWindow,
69+
loadImage,
70+
saveImage
6671
};

Diff for: lib/adapter/native.adapter.class.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jest.mock("../provider/native/clipboardy-clipboard.class");
1212
jest.mock("../provider/native/libnut-mouse.class");
1313
jest.mock("../provider/native/libnut-keyboard.class");
1414
jest.mock("../provider/native/libnut-window.class");
15+
jest.mock('jimp', () => {});
1516

1617
let clipboardMock: ClipboardAction;
1718
let keyboardMock: KeyboardAction;

Diff for: lib/adapter/vision.adapter.class.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {VisionAdapter} from "./vision.adapter.class";
66
import providerRegistry from "../provider/provider-registry.class";
77
import {MatchResult} from "../match-result.class";
88

9+
jest.mock('jimp', () => {});
910
jest.mock("../provider/native/libnut-screen.class");
1011

1112
const finderMock = {

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

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {Region} from "./region.class";
44
import {ScreenClass} from "./screen.class";
55
import providerRegistry from "./provider/provider-registry.class";
66

7+
jest.mock('jimp', () => {});
78
jest.mock("./adapter/native.adapter.class");
89
jest.mock("./adapter/vision.adapter.class");
910
jest.mock("./screen.class");

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

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {NativeAdapter} from "./adapter/native.adapter.class";
22
import {ClipboardClass} from "./clipboard.class";
33
import providerRegistry from "./provider/provider-registry.class";
44

5+
jest.mock('jimp', () => {});
56
jest.mock("./adapter/native.adapter.class");
67

78
beforeEach(() => {

Diff for: lib/expect/matchers/toBeAt.function.e2e.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { mouse } from "../../../index";
22
import { Point } from "../../point.class";
33
import { toBeAt } from "./toBeAt.function";
44

5+
jest.mock('jimp', () => {});
6+
57
const targetPoint = new Point(100, 100);
68

79
describe(".toBeAt", () => {

Diff for: lib/expect/matchers/toBeIn.function.e2e.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Point } from "../../point.class";
33
import { Region } from "../../region.class";
44
import { toBeIn } from "./toBeIn.function";
55

6+
jest.mock('jimp', () => {});
7+
68
const targetPoint = new Point(400, 400);
79

810
describe(".toBeIn", () => {

Diff for: lib/provider/io/__mocks__/calculator.png

463 Bytes
Loading

Diff for: lib/provider/io/jimp-image-reader.class.spec.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import ImageReader from "./jimp-image-reader.class";
2+
import {join} from "path";
3+
import Jimp from "jimp";
4+
5+
jest.mock('gifwrap', () => {});
6+
jest.mock('jimp', () => {
7+
class JimpMock {
8+
bitmap = {
9+
width: 100,
10+
height: 100,
11+
data: Buffer.from([]),
12+
}
13+
hasAlpha = () => false
14+
static read = jest.fn(() => Promise.resolve(new JimpMock()))
15+
}
16+
17+
return ({
18+
__esModule: true,
19+
default: JimpMock
20+
})
21+
});
22+
23+
afterEach(() => jest.resetAllMocks());
24+
25+
describe('Jimp image reader', () => {
26+
it('should return an Image object', async () => {
27+
// GIVEN
28+
const inputPath = join(__dirname, "__mocks__", "calculator.png");
29+
const scanMock = jest.fn();
30+
Jimp.prototype.scan = scanMock;
31+
const SUT = new ImageReader();
32+
33+
// WHEN
34+
await SUT.load(inputPath);
35+
36+
// THEN
37+
expect(scanMock).toHaveBeenCalledTimes(1);
38+
expect(Jimp.read).toBeCalledTimes(1);
39+
expect(Jimp.read).toBeCalledWith(inputPath);
40+
});
41+
42+
it('should reject on loading failures', async () => {
43+
// GIVEN
44+
const inputPath = "/some/path/to/file";
45+
const expectedError = "Error during load";
46+
const SUT = new ImageReader();
47+
Jimp.read = jest.fn(() => {
48+
throw new Error(expectedError);
49+
})
50+
51+
// WHEN
52+
try {
53+
await SUT.load(inputPath);
54+
} catch (err) {
55+
// THEN
56+
expect(err).toStrictEqual(Error(expectedError));
57+
}
58+
});
59+
});

Diff for: lib/provider/io/jimp-image-reader.class.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Jimp from 'jimp';
2+
import {ImageReader} from "../image-reader.type";
3+
import {Image} from "../../image.class";
4+
5+
export default class implements ImageReader {
6+
load(parameters: string): Promise<Image> {
7+
return new Promise<Image>((resolve, reject) => {
8+
Jimp.read(parameters)
9+
.then(jimpImage => {
10+
// stay consistent with images retrieved from libnut which uses BGR format
11+
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function(_, __, idx) {
12+
const red = this.bitmap.data[idx];
13+
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
14+
this.bitmap.data[idx + 2] = red;
15+
});
16+
resolve(new Image(
17+
jimpImage.bitmap.width,
18+
jimpImage.bitmap.height,
19+
jimpImage.bitmap.data,
20+
jimpImage.hasAlpha() ? 4 : 3
21+
));
22+
}).catch(err => reject(`Failed to load image from '${parameters}'. Reason: ${err}`));
23+
})
24+
}
25+
}

Diff for: lib/provider/io/jimp-image-writer.class.spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ImageWriter from "./jimp-image-writer.class";
2+
import {Image} from "../../image.class";
3+
import Jimp from "jimp";
4+
5+
jest.mock('gifwrap', () => {});
6+
jest.mock('jimp', () => {
7+
class JimpMock {
8+
bitmap = {
9+
width: 100,
10+
height: 100,
11+
data: Buffer.from([]),
12+
}
13+
hasAlpha = () => false
14+
static read = jest.fn(() => Promise.resolve(new JimpMock()))
15+
}
16+
17+
return ({
18+
__esModule: true,
19+
default: JimpMock
20+
})
21+
});
22+
23+
afterEach(() => jest.resetAllMocks());
24+
25+
describe('Jimp image writer', () => {
26+
it('should reject on writing failures', async () => {
27+
// GIVEN
28+
const outputFile = new Image(100, 200, Buffer.from([]), 3);
29+
const outputFileName = "/does/not/compute.png"
30+
const writeMock = jest.fn(() => Promise.resolve(new Jimp()));
31+
const scanMock = jest.fn();
32+
Jimp.prototype.scan = scanMock;
33+
Jimp.prototype.writeAsync = writeMock;
34+
const SUT = new ImageWriter();
35+
36+
// WHEN
37+
await SUT.store({data: outputFile, path: outputFileName});
38+
39+
// THEN
40+
expect(scanMock).toHaveBeenCalledTimes(1)
41+
expect(writeMock).toHaveBeenCalledTimes(1)
42+
expect(writeMock).toHaveBeenCalledWith(outputFileName)
43+
});
44+
});

Diff for: lib/provider/io/jimp-image-writer.class.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Jimp from 'jimp';
2+
import {ImageWriter, ImageWriterParameters} from "../image-writer.type";
3+
4+
export default class implements ImageWriter {
5+
store(parameters: ImageWriterParameters): Promise<void> {
6+
return new Promise((resolve, reject) => {
7+
const jimpImage = new Jimp({
8+
data: parameters.data.data,
9+
width: parameters.data.width,
10+
height: parameters.data.height
11+
});
12+
// libnut returns data in BGR format, so we have to switch red and blue color channels
13+
jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
14+
const red = this.bitmap.data[idx];
15+
this.bitmap.data[idx] = this.bitmap.data[idx + 2];
16+
this.bitmap.data[idx + 2] = red;
17+
});
18+
jimpImage
19+
.writeAsync(parameters.path)
20+
.then(_ => resolve())
21+
.catch(err => reject(err));
22+
});
23+
}
24+
}

Diff for: lib/provider/native/clipboardy-clipboard.class.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import ClipboardAction from "./clipboardy-clipboard.class";
22

3+
jest.mock('jimp', () => {});
4+
35
beforeEach(() => {
46
jest.resetAllMocks();
57
});

Diff for: lib/provider/provider-registry.class.ts

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import Window from "./native/libnut-window.class";
1313
import {ImageReader} from "./image-reader.type";
1414
import {ImageWriter} from "./image-writer.type";
1515

16+
import ImageReaderImpl from "./io/jimp-image-reader.class";
17+
import ImageWriterImpl from "./io/jimp-image-writer.class";
18+
1619
export interface ProviderRegistry {
1720
getClipboard(): ClipboardProviderInterface;
1821
registerClipboardProvider(value: ClipboardProviderInterface): void;
@@ -143,5 +146,7 @@ providerRegistry.registerKeyboardProvider(new Keyboard());
143146
providerRegistry.registerMouseProvider(new Mouse());
144147
providerRegistry.registerScreenProvider(new Screen());
145148
providerRegistry.registerWindowProvider(new Window());
149+
providerRegistry.registerImageWriter(new ImageWriterImpl());
150+
providerRegistry.registerImageReader(new ImageReaderImpl());
146151

147152
export default providerRegistry;

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

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { mockPartial } from "sneer";
1111
import { FileType } from "./file-type.enum";
1212
import providerRegistry from "./provider/provider-registry.class";
1313

14+
jest.mock('jimp', () => {});
1415
jest.mock("./adapter/native.adapter.class");
1516
jest.mock("./adapter/vision.adapter.class");
1617

Diff for: lib/sleep.function.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {busyWaitForNanoSeconds, sleep} from "./sleep.function";
22

33
const maxTimeDeltaInMs = 3;
44

5+
jest.mock('jimp', () => {});
6+
57
describe("sleep", () => {
68
it("should resolve after x ms", async () => {
79
// GIVEN

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

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Window} from "./window.class";
22
import {NativeAdapter} from "./adapter/native.adapter.class";
33
import providerRegistry from "./provider/provider-registry.class";
44

5+
jest.mock('jimp', () => {});
56
jest.mock("./adapter/native.adapter.class");
67

78
describe("Window class", () => {

Diff for: lib/window.function.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {NativeAdapter} from "./adapter/native.adapter.class";
33
import {Window} from "./window.class";
44
import providerRegistry from "./provider/provider-registry.class";
55

6+
jest.mock('jimp', () => {});
7+
68
describe("WindowApi", () => {
79
describe("getWindows", () => {
810
it("should return a list of open Windows", async () => {

0 commit comments

Comments
 (0)