generated from nhcarrigan/template
fa906684c2
## 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>
243 lines
9.7 KiB
TypeScript
243 lines
9.7 KiB
TypeScript
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!"
|
|
);
|
|
});
|
|
});
|
|
});
|