generated from nhcarrigan/template
feat: multiple UI improvements, font settings, and memory file display names (#175)
## Summary - **fix**: `show_thinking_blocks` setting now persists across sessions — it was defined on the TypeScript side but missing from the Rust `HikariConfig` struct, so serde silently dropped it on every save/load - **feat**: Tool calls are now rendered as collapsible blocks matching the Extended Thinking block aesthetic, replacing the old inline dropdown approach - **feat**: Add configurable max output tokens setting - **feat**: Use random creative names for conversation tabs - **test**: Significantly expanded frontend unit test coverage - **docs**: Require tests for all changes in CLAUDE.md - **feat**: Allow users to specify a custom terminal font (Closes #176) - **feat**: Display friendly names for memory files derived from the first heading (Closes #177) - **feat**: Add custom UI font support for the app chrome (buttons, labels, tabs) - **fix**: Apply custom UI font to the full app interface — `.app-container` was hardcoded, blocking inheritance from `body`; also renamed "Custom Font" to "Custom Terminal Font" for clarity ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #175 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #175.
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { isPermissionGranted } from "@tauri-apps/plugin-notification";
|
||||
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||
import { notificationManager } from "./notificationManager";
|
||||
import { sendTerminalNotification } from "./terminalNotifier";
|
||||
import { NotificationType, NOTIFICATION_SOUNDS } from "./types";
|
||||
|
||||
vi.mock("./soundPlayer", () => ({
|
||||
soundPlayer: { play: vi.fn().mockResolvedValue(undefined) },
|
||||
}));
|
||||
|
||||
vi.mock("./terminalNotifier", () => ({
|
||||
sendTerminalNotification: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("NotificationManager", () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("notify()", () => {
|
||||
it("calls the first notification method by default", async () => {
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
"send_windows_notification",
|
||||
expect.objectContaining({
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("passes a custom message to the notification", async () => {
|
||||
await notificationManager.notify(NotificationType.ERROR, "Custom error message");
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.ERROR].phrase,
|
||||
body: "Custom error message",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the next method when the first fails", async () => {
|
||||
setMockInvokeResult("send_windows_notification", new Error("Method 1 failed"));
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_toast", expect.any(Object));
|
||||
});
|
||||
|
||||
it("falls back to terminal notification when all methods fail", async () => {
|
||||
setMockInvokeResult("send_windows_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_windows_toast", new Error("failed"));
|
||||
setMockInvokeResult("send_wsl_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_notify_send", new Error("failed"));
|
||||
vi.mocked(isPermissionGranted).mockRejectedValueOnce(new Error("Permission check failed"));
|
||||
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
|
||||
expect(sendTerminalNotification).toHaveBeenCalledWith(
|
||||
NotificationType.SUCCESS,
|
||||
"Task completed successfully!"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the default SUCCESS message when none is provided", async () => {
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Task completed successfully!",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default ERROR message", async () => {
|
||||
await notificationManager.notify(NotificationType.ERROR);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Something went wrong...",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default PERMISSION message", async () => {
|
||||
await notificationManager.notify(NotificationType.PERMISSION);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Permission needed to continue",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default CONNECTION message", async () => {
|
||||
await notificationManager.notify(NotificationType.CONNECTION);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Successfully connected to Claude Code",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default TASK_START message", async () => {
|
||||
await notificationManager.notify(NotificationType.TASK_START);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Starting task...",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default COST_ALERT message", async () => {
|
||||
await notificationManager.notify(NotificationType.COST_ALERT);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "You've exceeded your cost threshold!",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the fallback default message for unhandled types", async () => {
|
||||
await notificationManager.notify(NotificationType.ACHIEVEMENT);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Notification",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("helper methods", () => {
|
||||
it("notifySuccess calls notify with SUCCESS type and default message", async () => {
|
||||
await notificationManager.notifySuccess();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase,
|
||||
body: "Task completed successfully!",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifySuccess passes a custom message", async () => {
|
||||
await notificationManager.notifySuccess("All done!");
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "All done!",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyError calls notify with ERROR type", async () => {
|
||||
await notificationManager.notifyError();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.ERROR].phrase,
|
||||
body: "Something went wrong...",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyPermission calls notify with PERMISSION type", async () => {
|
||||
await notificationManager.notifyPermission();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.PERMISSION].phrase,
|
||||
body: "Permission needed to continue",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyConnection calls notify with CONNECTION type", async () => {
|
||||
await notificationManager.notifyConnection();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.CONNECTION].phrase,
|
||||
body: "Successfully connected to Claude Code",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyTaskStart calls notify with TASK_START type", async () => {
|
||||
await notificationManager.notifyTaskStart();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.TASK_START].phrase,
|
||||
body: "Starting task...",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyCostAlert calls notify with COST_ALERT type", async () => {
|
||||
await notificationManager.notifyCostAlert();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.COST_ALERT].phrase,
|
||||
body: "You've exceeded your cost threshold!",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tauri plugin notification method (Method 4)", () => {
|
||||
it("sends notification when permission is already granted", async () => {
|
||||
const { isPermissionGranted, sendNotification } =
|
||||
await import("@tauri-apps/plugin-notification");
|
||||
setMockInvokeResult("send_windows_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_windows_toast", new Error("failed"));
|
||||
setMockInvokeResult("send_wsl_notification", new Error("failed"));
|
||||
vi.mocked(isPermissionGranted).mockResolvedValueOnce(true);
|
||||
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
|
||||
expect(sendNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("requests permission and sends notification when not yet granted", async () => {
|
||||
const { isPermissionGranted, requestPermission, sendNotification } =
|
||||
await import("@tauri-apps/plugin-notification");
|
||||
setMockInvokeResult("send_windows_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_windows_toast", new Error("failed"));
|
||||
setMockInvokeResult("send_wsl_notification", new Error("failed"));
|
||||
vi.mocked(isPermissionGranted).mockResolvedValueOnce(false);
|
||||
vi.mocked(requestPermission).mockResolvedValueOnce("granted");
|
||||
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
|
||||
expect(requestPermission).toHaveBeenCalledWith();
|
||||
expect(sendNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: "Task completed successfully!" })
|
||||
);
|
||||
});
|
||||
|
||||
it("falls through to next method when permission is denied", async () => {
|
||||
const { isPermissionGranted, requestPermission } =
|
||||
await import("@tauri-apps/plugin-notification");
|
||||
setMockInvokeResult("send_windows_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_windows_toast", new Error("failed"));
|
||||
setMockInvokeResult("send_wsl_notification", new Error("failed"));
|
||||
vi.mocked(isPermissionGranted).mockResolvedValueOnce(false);
|
||||
vi.mocked(requestPermission).mockResolvedValueOnce("denied");
|
||||
setMockInvokeResult("send_notify_send", new Error("failed"));
|
||||
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
|
||||
expect(sendTerminalNotification).toHaveBeenCalledWith(
|
||||
NotificationType.SUCCESS,
|
||||
"Task completed successfully!"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -232,6 +232,53 @@ describe("notifications", () => {
|
||||
// Should not throw
|
||||
await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("play warns when audio type is not in the cache", async () => {
|
||||
const { soundPlayer } = await import("./soundPlayer");
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
soundPlayer.setEnabled(true);
|
||||
await soundPlayer.play("nonexistent" as NotificationType);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith("No audio found for notification type: nonexistent");
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("play catches errors from audio playback", async () => {
|
||||
vi.resetModules();
|
||||
|
||||
class FailingAudio {
|
||||
volume = 1;
|
||||
preload = "auto";
|
||||
|
||||
cloneNode() {
|
||||
const clone = new FailingAudio();
|
||||
clone.volume = this.volume;
|
||||
return clone;
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
throw new Error("Playback blocked by browser");
|
||||
}
|
||||
}
|
||||
|
||||
const originalAudio = globalThis.Audio;
|
||||
globalThis.Audio = FailingAudio as unknown as typeof Audio;
|
||||
|
||||
const { soundPlayer: freshPlayer } = await import("./soundPlayer");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
freshPlayer.setEnabled(true);
|
||||
await freshPlayer.play(NotificationType.SUCCESS);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to play notification sound:",
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
globalThis.Audio = originalAudio;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationManager class", () => {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Notification Rules Tests
|
||||
*
|
||||
* Tests the connection status change handler, which fires a connection
|
||||
* notification sound exactly once per reconnect cycle.
|
||||
*
|
||||
* What this module does:
|
||||
* - Tracks the previous connection status in module-level state
|
||||
* - Fires a notification only when transitioning from a non-connected
|
||||
* state (disconnected/connecting) to "connected"
|
||||
* - Ignores the initial connection (null → connected) to avoid noisy
|
||||
* notifications on app start
|
||||
* - Provides no-op handlers for tool execution and user messages
|
||||
* (reserved for future notification rules)
|
||||
* - cleanupNotificationRules() resets tracking state on teardown
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const { mockNotifyConnection } = vi.hoisted(() => ({
|
||||
mockNotifyConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./notificationManager", () => ({
|
||||
notificationManager: {
|
||||
notifyConnection: mockNotifyConnection,
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
handleConnectionStatusChange,
|
||||
handleToolExecution,
|
||||
handleNewUserMessage,
|
||||
initializeNotificationRules,
|
||||
cleanupNotificationRules,
|
||||
} from "./rules";
|
||||
|
||||
// ---
|
||||
|
||||
describe("handleConnectionStatusChange", () => {
|
||||
beforeEach(() => {
|
||||
mockNotifyConnection.mockReset();
|
||||
cleanupNotificationRules(); // Reset module-level previousConnectionStatus to null
|
||||
});
|
||||
|
||||
describe("initial connection (null → status)", () => {
|
||||
it("does not notify on first connection (null → connected)", () => {
|
||||
// previousConnectionStatus is null (falsy), so condition is not met
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not notify when disconnecting from initial state (null → disconnected)", () => {
|
||||
handleConnectionStatusChange("disconnected");
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not notify when entering connecting from initial state (null → connecting)", () => {
|
||||
handleConnectionStatusChange("connecting");
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconnection (disconnected → connected)", () => {
|
||||
it("notifies when reconnecting after a disconnection", () => {
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("notifies exactly once per reconnect", () => {
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconnection (connecting → connected)", () => {
|
||||
it("notifies when transitioning from connecting to connected", () => {
|
||||
handleConnectionStatusChange("connecting");
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe("already connected (connected → connected)", () => {
|
||||
it("does not notify when already connected", () => {
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected"); // First connection — notifies
|
||||
mockNotifyConnection.mockReset();
|
||||
|
||||
handleConnectionStatusChange("connected"); // Second — same status, no notify
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("disconnecting (connected → disconnected)", () => {
|
||||
it("does not notify when disconnecting", () => {
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected");
|
||||
mockNotifyConnection.mockReset();
|
||||
|
||||
handleConnectionStatusChange("disconnected");
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple reconnect cycles", () => {
|
||||
it("notifies once per reconnect cycle", () => {
|
||||
// First cycle
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockNotifyConnection.mockReset();
|
||||
|
||||
// Second cycle
|
||||
handleConnectionStatusChange("disconnected");
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanupNotificationRules", () => {
|
||||
it("resets state so the next connection is treated as the first", () => {
|
||||
// Establish a known previous status
|
||||
handleConnectionStatusChange("disconnected");
|
||||
// Now cleanup
|
||||
cleanupNotificationRules();
|
||||
// After cleanup, previousConnectionStatus is null again
|
||||
// So the next "connected" should NOT notify (treated as initial connection)
|
||||
handleConnectionStatusChange("connected");
|
||||
expect(mockNotifyConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("no-op handlers", () => {
|
||||
it("handleToolExecution does not throw", () => {
|
||||
expect(() => handleToolExecution("Bash")).not.toThrow();
|
||||
});
|
||||
|
||||
it("handleNewUserMessage does not throw", () => {
|
||||
expect(() => handleNewUserMessage()).not.toThrow();
|
||||
});
|
||||
|
||||
it("initializeNotificationRules does not throw", () => {
|
||||
expect(() => initializeNotificationRules()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NotificationType } from "./types";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
import { sendTerminalNotification } from "./terminalNotifier";
|
||||
|
||||
vi.mock("$lib/stores/claude", () => ({
|
||||
claudeStore: {
|
||||
addLine: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("sendTerminalNotification", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("adds a system line for success type with sparkle emoji", () => {
|
||||
sendTerminalNotification(NotificationType.SUCCESS);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("✨"));
|
||||
});
|
||||
|
||||
it("adds a system line for error type with cross emoji", () => {
|
||||
sendTerminalNotification(NotificationType.ERROR);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("❌"));
|
||||
});
|
||||
|
||||
it("adds a system line for permission type with lock emoji", () => {
|
||||
sendTerminalNotification(NotificationType.PERMISSION);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🔐"));
|
||||
});
|
||||
|
||||
it("adds a system line for connection type with link emoji", () => {
|
||||
sendTerminalNotification(NotificationType.CONNECTION);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🔗"));
|
||||
});
|
||||
|
||||
it("adds a system line for task_start type with rocket emoji", () => {
|
||||
sendTerminalNotification(NotificationType.TASK_START);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🚀"));
|
||||
});
|
||||
|
||||
it("includes the optional message in the notification", () => {
|
||||
sendTerminalNotification(NotificationType.SUCCESS, "Custom message text");
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith(
|
||||
"system",
|
||||
expect.stringContaining("Custom message text")
|
||||
);
|
||||
});
|
||||
|
||||
it("includes the sound phrase as the notification title", () => {
|
||||
sendTerminalNotification(NotificationType.SUCCESS);
|
||||
|
||||
expect(claudeStore.addLine).toHaveBeenCalledWith(
|
||||
"system",
|
||||
expect.stringContaining("I'm done!")
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { testAllNotifications } from "./testNotifications";
|
||||
|
||||
vi.mock("./notificationManager", () => ({
|
||||
notificationManager: {
|
||||
notifySuccess: vi.fn().mockResolvedValue(undefined),
|
||||
notifyError: vi.fn().mockResolvedValue(undefined),
|
||||
notifyPermission: vi.fn().mockResolvedValue(undefined),
|
||||
notifyConnection: vi.fn().mockResolvedValue(undefined),
|
||||
notifyTaskStart: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("testNotifications", () => {
|
||||
describe("window assignment", () => {
|
||||
it("assigns testAllNotifications to window.testNotifications", async () => {
|
||||
// The module-level if block runs on import — reimport to ensure it ran
|
||||
await import("./testNotifications");
|
||||
|
||||
expect((window as unknown as { testNotifications: unknown }).testNotifications).toBe(
|
||||
testAllNotifications
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("testAllNotifications", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("is an async function", () => {
|
||||
expect(typeof testAllNotifications).toBe("function");
|
||||
});
|
||||
|
||||
it("schedules all five notification type calls", async () => {
|
||||
const { notificationManager } = await import("./notificationManager");
|
||||
|
||||
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
await testAllNotifications();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(notificationManager.notifySuccess).toHaveBeenCalledWith("Test task completed!");
|
||||
expect(notificationManager.notifyError).toHaveBeenCalledWith("Test error occurred!");
|
||||
expect(notificationManager.notifyPermission).toHaveBeenCalledWith("Test permission request!");
|
||||
expect(notificationManager.notifyConnection).toHaveBeenCalledWith(
|
||||
"Test connection established!"
|
||||
);
|
||||
expect(notificationManager.notifyTaskStart).toHaveBeenCalledWith("Test task starting!");
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { sendWSLNotification } from "./wslNotificationHelper";
|
||||
|
||||
// platform() is mocked in vitest.setup.ts to return "linux" by default
|
||||
|
||||
describe("sendWSLNotification", () => {
|
||||
it("invokes send_windows_notification when platform is windows", async () => {
|
||||
vi.mocked(platform).mockResolvedValueOnce("windows" as Awaited<ReturnType<typeof platform>>);
|
||||
|
||||
await sendWSLNotification("Test Title", "Test body");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: "Test Title",
|
||||
body: "Test body",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not invoke on non-Windows platforms", async () => {
|
||||
// Default mock returns "linux"
|
||||
await sendWSLNotification("Test Title", "Test body");
|
||||
|
||||
expect(invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles invoke errors gracefully and logs them", async () => {
|
||||
vi.mocked(platform).mockResolvedValueOnce("windows" as Awaited<ReturnType<typeof platform>>);
|
||||
vi.mocked(invoke).mockRejectedValueOnce(new Error("Windows notification failed"));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await sendWSLNotification("Title", "Body");
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to send Windows notification:",
|
||||
expect.any(Error)
|
||||
);
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user