generated from nhcarrigan/template
test: add coverage for drafts, soundPlayer, wslNotificationHelper, and costTrackingStore
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user