test: add coverage for drafts, soundPlayer, wslNotificationHelper, and costTrackingStore

This commit is contained in:
2026-03-03 14:58:49 -08:00
committed by Naomi Carrigan
parent 58f53a421b
commit c819adc9ea
4 changed files with 293 additions and 1 deletions
@@ -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,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();
});
});
+196 -1
View File
@@ -1,12 +1,23 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { setMockInvokeResult } from "../../../vitest.setup";
import {
formatCost,
formatAlertType,
getAlertMessage,
costTrackingStore,
formattedCosts,
type AlertType,
type CostAlert,
} from "./costTracking";
vi.mock("$lib/notifications/notificationManager", () => ({
notificationManager: {
notifyCostAlert: vi.fn(),
},
}));
describe("formatCost", () => {
it("formats amounts below $0.01 to 4 decimal places", () => {
expect(formatCost(0)).toBe("$0.0000");
@@ -95,3 +106,187 @@ describe("getAlertMessage", () => {
expect(message).toContain("$0.0050");
});
});
describe("costTrackingStore", () => {
beforeEach(() => {
vi.clearAllMocks();
costTrackingStore.reset();
});
describe("refresh", () => {
it("loads cost data from the backend", async () => {
setMockInvokeResult("get_today_cost", 1.5);
setMockInvokeResult("get_week_cost", 5.25);
setMockInvokeResult("get_month_cost", 20.0);
setMockInvokeResult("get_cost_alerts", []);
await costTrackingStore.refresh();
const state = get(costTrackingStore);
expect(state.todayCost).toBe(1.5);
expect(state.weekCost).toBe(5.25);
expect(state.monthCost).toBe(20.0);
expect(state.isLoading).toBe(false);
});
it("triggers notifications for any returned alerts", async () => {
const { notificationManager } = await import("$lib/notifications/notificationManager");
const alert: CostAlert = { alert_type: "Daily", threshold: 1.0, current_cost: 1.5 };
setMockInvokeResult("get_today_cost", 1.5);
setMockInvokeResult("get_week_cost", 0);
setMockInvokeResult("get_month_cost", 0);
setMockInvokeResult("get_cost_alerts", [alert]);
await costTrackingStore.refresh();
expect(notificationManager.notifyCostAlert).toHaveBeenCalledWith(
expect.stringContaining("Today")
);
});
it("returns alerts from refresh", async () => {
const alert: CostAlert = { alert_type: "Weekly", threshold: 5.0, current_cost: 6.0 };
setMockInvokeResult("get_today_cost", 0);
setMockInvokeResult("get_week_cost", 6.0);
setMockInvokeResult("get_month_cost", 0);
setMockInvokeResult("get_cost_alerts", [alert]);
const result = await costTrackingStore.refresh();
expect(result).toEqual([alert]);
});
it("handles refresh errors gracefully and returns empty array", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("get_today_cost", new Error("Backend error"));
const result = await costTrackingStore.refresh();
expect(result).toEqual([]);
expect(errorSpy).toHaveBeenCalledWith("Failed to refresh cost tracking:", expect.any(Error));
errorSpy.mockRestore();
});
});
describe("getSummary", () => {
it("fetches and stores a cost summary", async () => {
const mockSummary = {
period_days: 7,
total_input_tokens: 1000,
total_output_tokens: 500,
total_cost: 0.05,
total_messages: 20,
total_sessions: 3,
average_daily_cost: 0.007,
daily_breakdown: [],
};
setMockInvokeResult("get_cost_summary", mockSummary);
const result = await costTrackingStore.getSummary(7);
expect(result).toEqual(mockSummary);
expect(invoke).toHaveBeenCalledWith("get_cost_summary", { days: 7 });
});
it("returns null and logs error on failure", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("get_cost_summary", new Error("Summary unavailable"));
const result = await costTrackingStore.getSummary(30);
expect(result).toBeNull();
expect(errorSpy).toHaveBeenCalledWith("Failed to get cost summary:", expect.any(Error));
errorSpy.mockRestore();
});
});
describe("setAlertThresholds", () => {
it("persists new thresholds to the backend", async () => {
const thresholds = { daily: 2.0, weekly: 10.0, monthly: 40.0 };
await costTrackingStore.setAlertThresholds(thresholds);
expect(invoke).toHaveBeenCalledWith("set_cost_alert_thresholds", {
daily: 2.0,
weekly: 10.0,
monthly: 40.0,
});
});
it("logs an error when the backend call fails", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("set_cost_alert_thresholds", new Error("Failed"));
await costTrackingStore.setAlertThresholds({ daily: 1.0, weekly: null, monthly: null });
expect(errorSpy).toHaveBeenCalledWith("Failed to set alert thresholds:", expect.any(Error));
errorSpy.mockRestore();
});
});
describe("exportCsv", () => {
it("returns the CSV string from the backend", async () => {
setMockInvokeResult("export_cost_csv", "date,cost\n2026-01-01,1.50");
const result = await costTrackingStore.exportCsv(7);
expect(result).toBe("date,cost\n2026-01-01,1.50");
expect(invoke).toHaveBeenCalledWith("export_cost_csv", { days: 7 });
});
it("returns null and logs error on failure", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
setMockInvokeResult("export_cost_csv", new Error("Export failed"));
const result = await costTrackingStore.exportCsv(30);
expect(result).toBeNull();
expect(errorSpy).toHaveBeenCalledWith("Failed to export CSV:", expect.any(Error));
errorSpy.mockRestore();
});
});
describe("reset", () => {
it("resets costs back to zero", async () => {
setMockInvokeResult("get_today_cost", 5.0);
setMockInvokeResult("get_week_cost", 15.0);
setMockInvokeResult("get_month_cost", 50.0);
setMockInvokeResult("get_cost_alerts", []);
await costTrackingStore.refresh();
costTrackingStore.reset();
const state = get(costTrackingStore);
expect(state.todayCost).toBe(0);
expect(state.weekCost).toBe(0);
expect(state.monthCost).toBe(0);
});
});
});
describe("formattedCosts", () => {
beforeEach(() => {
costTrackingStore.reset();
});
it("formats the initial zero costs correctly", () => {
const costs = get(formattedCosts);
expect(costs.today).toBe("$0.0000");
expect(costs.week).toBe("$0.0000");
expect(costs.month).toBe("$0.0000");
});
it("reflects updated costs after a refresh", async () => {
setMockInvokeResult("get_today_cost", 1.5);
setMockInvokeResult("get_week_cost", 5.0);
setMockInvokeResult("get_month_cost", 20.0);
setMockInvokeResult("get_cost_alerts", []);
await costTrackingStore.refresh();
const costs = get(formattedCosts);
expect(costs.today).toBe("$1.50");
expect(costs.week).toBe("$5.00");
expect(costs.month).toBe("$20.00");
expect(costs.todayRaw).toBe(1.5);
});
});
+10
View File
@@ -180,6 +180,16 @@ describe("draftsStore", () => {
const result = draftsStore.formatTimestamp(invalid);
expect(result).toBe(invalid);
});
it("falls back to raw string when toLocaleString throws", () => {
const spy = vi.spyOn(Date.prototype, "toLocaleString").mockImplementation(() => {
throw new Error("Locale not supported");
});
const ts = "2026-01-15T14:30:00.000Z";
const result = draftsStore.formatTimestamp(ts);
expect(result).toBe(ts);
spy.mockRestore();
});
});
});