Skip to content

Commit 7eeef0a

Browse files
committed
test: add unit tests for RestApi, Tray, and Updater modules
- Created tests for RestApiModule and RestApiService to verify metadata registration and API response handling. - Added tests for TrayModule and TrayService to ensure correct menu setup and destruction. - Implemented tests for UpdaterModule and related services to validate update checks and notifications. - Organized all test files into a structured __tests__ directory mirroring the source code structure.
1 parent 516de74 commit 7eeef0a

50 files changed

Lines changed: 2315 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"preview": "vite preview",
2121
"test:e2e": "playwright test",
2222
"test:unit:renderer": "vitest src/renderer --watch=false",
23-
"test:unit:main": "vitest src/main --watch=false",
23+
"test:unit:main": "vitest --config vitest.config.main.ts --watch=false",
2424
"check:port": "netstat -aon | findstr :8844",
2525
"free:port": "tasklist /fi \"PID eq 19680\" ; taskkill /PID 19680 /F",
2626
"open:mac:prod": "./dist/mac/reminder.app/Contents/MacOS/reminder",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { showErrorMessages } from "./error-messages.js";
4+
5+
vi.mock("electron", () => ({
6+
dialog: {
7+
showMessageBox: vi.fn(),
8+
},
9+
}));
10+
11+
vi.mock("./logger.js", () => ({
12+
logger: {
13+
error: vi.fn(),
14+
},
15+
}));
16+
17+
describe("@shared/error-messages", () => {
18+
it("logs error and shows dialog by default", async () => {
19+
showErrorMessages({ title: "Oops", body: "Something failed" });
20+
21+
const { dialog } = await import("electron");
22+
const { logger } = await import("./logger.js");
23+
24+
expect(logger.error).toHaveBeenCalledWith("Oops", "Something failed");
25+
expect(dialog.showMessageBox).toHaveBeenCalledWith({
26+
title: "Oops",
27+
message: "Something failed",
28+
});
29+
});
30+
31+
it("logs error without showing dialog when isDialog is false", async () => {
32+
showErrorMessages({ title: "Oops", body: "Hidden", isDialog: false });
33+
34+
const { dialog } = await import("electron");
35+
const { logger } = await import("./logger.js");
36+
37+
expect(logger.error).toHaveBeenCalledWith("Oops", "Hidden");
38+
expect(dialog.showMessageBox).not.toHaveBeenCalled();
39+
});
40+
});

src/main/@shared/ipc/ipc.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import { ipcMainHandle, ipcMainOn, validateEventFrame } from "./ipc.js";
4+
5+
vi.mock("electron", () => ({
6+
ipcMain: {
7+
handle: vi.fn(),
8+
on: vi.fn(),
9+
},
10+
}));
11+
12+
describe("@shared/ipc", () => {
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
});
16+
17+
describe("ipcMainHandle", () => {
18+
it("registers handler and validates frame", async () => {
19+
const handler = vi.fn().mockResolvedValue({ ok: true });
20+
const mockFrame = { url: "file:///window:main#/" };
21+
const event = { senderFrame: mockFrame };
22+
23+
ipcMainHandle("getAppVersion", handler);
24+
25+
const { ipcMain } = await import("electron");
26+
const registeredHandler = vi.mocked(ipcMain.handle).mock.calls[0][1];
27+
28+
const result = await registeredHandler(event, "payload");
29+
30+
expect(handler).toHaveBeenCalledWith("payload");
31+
expect(result).toEqual({ ok: true });
32+
});
33+
34+
it("throws when frame is null", async () => {
35+
const handler = vi.fn();
36+
const event = { senderFrame: null };
37+
38+
ipcMainHandle("getAppVersion", handler);
39+
40+
const { ipcMain } = await import("electron");
41+
const registeredHandler = vi.mocked(ipcMain.handle).mock.calls[0][1];
42+
43+
await expect(registeredHandler(event, undefined)).rejects.toThrow(
44+
"Invalid frame: Frame is null",
45+
);
46+
});
47+
});
48+
49+
describe("ipcMainOn", () => {
50+
it("registers channel and calls callback", async () => {
51+
const callback = vi.fn();
52+
53+
ipcMainOn("logout", callback);
54+
55+
const { ipcMain } = await import("electron");
56+
const registeredHandler = vi.mocked(ipcMain.on).mock.calls[0][1];
57+
const event = { reply: vi.fn() } as any;
58+
59+
registeredHandler(event, { id: "123" });
60+
61+
expect(callback).toHaveBeenCalledWith(event, { id: "123" });
62+
});
63+
});
64+
65+
describe("validateEventFrame", () => {
66+
it("allows file protocol frames with hash", () => {
67+
const frame = { url: "file:///window:main#/" } as any;
68+
69+
expect(() => validateEventFrame(frame)).not.toThrow();
70+
});
71+
72+
it("allows localhost in development", () => {
73+
process.env.NODE_ENV = "development";
74+
process.env.LOCALHOST_ELECTRON_SERVER_PORT = "8844";
75+
const frame = { url: "http://localhost:8844/" } as any;
76+
77+
expect(() => validateEventFrame(frame)).not.toThrow();
78+
});
79+
80+
it("rejects unauthorized frames", () => {
81+
const frame = { url: "https://evil.com/" } as any;
82+
83+
expect(() => validateEventFrame(frame)).toThrow(
84+
"The event is from an unauthorized frame",
85+
);
86+
});
87+
});
88+
});

src/main/@shared/logger.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
type TTransport = {
4+
format?: string;
5+
level?: string;
6+
maxSize?: number;
7+
};
8+
9+
describe("@shared/logger", () => {
10+
it("configures transports for development", async () => {
11+
process.env.NODE_ENV = "development";
12+
vi.resetModules();
13+
14+
const fileTransport: TTransport = {};
15+
const consoleTransport: TTransport = {};
16+
17+
vi.doMock("electron-log", () => ({
18+
default: {
19+
info: vi.fn(),
20+
transports: {
21+
file: fileTransport,
22+
console: consoleTransport,
23+
},
24+
},
25+
}));
26+
27+
const { logger } = await import("./logger.js");
28+
29+
expect(logger).toBeDefined();
30+
expect(fileTransport.level).toBe("info");
31+
expect(consoleTransport.level).toBe("debug");
32+
expect(fileTransport.maxSize).toBe(5 * 1024 * 1024);
33+
});
34+
35+
it("configures transports for production", async () => {
36+
process.env.NODE_ENV = "production";
37+
vi.resetModules();
38+
39+
const fileTransport: TTransport = {};
40+
const consoleTransport: TTransport = {};
41+
42+
vi.doMock("electron-log", () => ({
43+
default: {
44+
info: vi.fn(),
45+
transports: {
46+
file: fileTransport,
47+
console: consoleTransport,
48+
},
49+
},
50+
}));
51+
52+
const { logger } = await import("./logger.js");
53+
54+
expect(logger).toBeDefined();
55+
expect(fileTransport.level).toBe("info");
56+
expect(consoleTransport.level).toBe("info");
57+
});
58+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("electron", () => ({
4+
app: {
5+
getAppPath: vi.fn(() => "/app"),
6+
},
7+
}));
8+
9+
vi.mock("./utils.js", () => ({
10+
isDev: vi.fn(() => false),
11+
}));
12+
13+
describe("@shared/path-resolver", () => {
14+
it("resolves preload path for production", async () => {
15+
vi.resetModules();
16+
const { getPreloadPath } = await import("./path-resolver.js");
17+
expect(getPreloadPath()).toBe("/dist-main/preload.cjs");
18+
});
19+
20+
it("resolves UI path", async () => {
21+
vi.resetModules();
22+
const { getUIPath } = await import("./path-resolver.js");
23+
expect(getUIPath()).toBe("/app/dist-renderer/index.html");
24+
});
25+
26+
it("resolves assets path for development", async () => {
27+
vi.resetModules();
28+
const { isDev } = await import("./utils.js");
29+
vi.mocked(isDev).mockReturnValue(true);
30+
31+
const { getAssetsPath } = await import("./path-resolver.js");
32+
expect(getAssetsPath()).toBe("/app/src/assets");
33+
});
34+
});

src/main/@shared/store.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
clearElectronStorage,
5+
clearStore,
6+
deleteFromElectronStorage,
7+
deleteStore,
8+
getElectronStorage,
9+
getStore,
10+
hasElectronStorage,
11+
hasStore,
12+
setElectronStorage,
13+
setStore,
14+
} from "./store.js";
15+
16+
describe("@shared/store", () => {
17+
beforeEach(() => {
18+
clearStore();
19+
});
20+
21+
describe("in-memory store", () => {
22+
it("sets and gets values", () => {
23+
setStore("testKey", "testValue");
24+
expect(getStore("testKey")).toBe("testValue");
25+
});
26+
27+
it("checks if a key exists", () => {
28+
setStore("testKey", "value");
29+
expect(hasStore("testKey")).toBe(true);
30+
expect(hasStore("missingKey")).toBe(false);
31+
});
32+
33+
it("deletes values", () => {
34+
setStore("testKey", "value");
35+
deleteStore("testKey");
36+
expect(hasStore("testKey")).toBe(false);
37+
});
38+
39+
it("clears all values", () => {
40+
setStore("key1", "value1");
41+
setStore("key2", "value2");
42+
clearStore();
43+
expect(hasStore("key1")).toBe(false);
44+
expect(hasStore("key2")).toBe(false);
45+
});
46+
});
47+
48+
describe("electron storage", () => {
49+
it("interacts with electron-store", async () => {
50+
setElectronStorage("authToken", "token-123");
51+
getElectronStorage("authToken");
52+
hasElectronStorage("authToken");
53+
deleteFromElectronStorage("authToken");
54+
clearElectronStorage();
55+
56+
const { getLastElectronStoreInstance } = await import("electron-store");
57+
const instance = getLastElectronStoreInstance();
58+
59+
expect(instance?.set).toHaveBeenCalledWith("authToken", "token-123");
60+
expect(instance?.get).toHaveBeenCalledWith("authToken");
61+
expect(instance?.has).toHaveBeenCalledWith("authToken");
62+
expect(instance?.delete).toHaveBeenCalledWith("authToken");
63+
expect(instance?.clear).toHaveBeenCalled();
64+
});
65+
});
66+
});

src/main/@shared/utils.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { isDev, isPlatform } from "./utils.js";
4+
5+
describe("@shared/utils", () => {
6+
describe("isDev", () => {
7+
it("returns true when NODE_ENV is development", () => {
8+
process.env.NODE_ENV = "development";
9+
expect(isDev()).toBe(true);
10+
});
11+
12+
it("returns false when NODE_ENV is not development", () => {
13+
process.env.NODE_ENV = "production";
14+
expect(isDev()).toBe(false);
15+
});
16+
});
17+
18+
describe("isPlatform", () => {
19+
it("returns true for current platform", () => {
20+
expect(isPlatform(process.platform)).toBe(true);
21+
});
22+
23+
it("returns false for different platform", () => {
24+
const otherPlatform = process.platform === "win32" ? "darwin" : "win32";
25+
expect(isPlatform(otherPlatform)).toBe(false);
26+
});
27+
});
28+
});

src/main/app-preload/ipc.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("@devisfuture/electron-modular", () => ({
4+
IpcHandler: () => () => undefined,
5+
}));
6+
7+
describe("AppPreloadIpc", () => {
8+
it("creates preload window on init", async () => {
9+
const { AppPreloadIpc } = await import("./ipc.js");
10+
11+
const create = vi.fn();
12+
const getWindow = vi.fn().mockReturnValue({ create });
13+
14+
const ipc = new AppPreloadIpc();
15+
ipc.onInit({ getWindow } as any);
16+
17+
expect(getWindow).toHaveBeenCalledWith("window:preload-app");
18+
expect(create).toHaveBeenCalled();
19+
});
20+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import "reflect-metadata";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { AppPreloadIpc } from "./ipc.js";
5+
import { AppPreloadModule } from "./module.js";
6+
import { AppPreloadWindow } from "./window.js";
7+
8+
describe("AppPreloadModule", () => {
9+
it("registers module metadata", () => {
10+
const metadata = Reflect.getMetadata("RgModule", AppPreloadModule);
11+
12+
expect(metadata).toBeDefined();
13+
expect(metadata.ipc).toEqual([AppPreloadIpc]);
14+
expect(metadata.windows).toEqual([AppPreloadWindow]);
15+
});
16+
});

0 commit comments

Comments
 (0)