generated from nhcarrigan/template
test: add coverage for debugConsole, notificationManager, and achievements
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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!",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { setMockInvokeResult, emitMockEvent } from "../../../vitest.setup";
|
||||
import {
|
||||
achievementsStore,
|
||||
unlockedAchievements,
|
||||
lockedAchievements,
|
||||
achievementsByRarity,
|
||||
achievementProgress,
|
||||
initAchievementsListener,
|
||||
} from "./achievements";
|
||||
import type { AchievementUnlockedEvent } from "$lib/types/achievements";
|
||||
import { playAchievementSound } from "$lib/sounds/achievement";
|
||||
|
||||
vi.mock("$lib/sounds/achievement", () => ({
|
||||
playAchievementSound: vi.fn(),
|
||||
}));
|
||||
|
||||
// Helper to build a minimal unlock event
|
||||
function makeEvent(id: AchievementUnlockedEvent["achievement"]["id"]): AchievementUnlockedEvent {
|
||||
return {
|
||||
achievement: {
|
||||
id,
|
||||
name: "Test",
|
||||
description: "Test achievement",
|
||||
icon: "🏆",
|
||||
unlocked_at: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("achievementsStore initial state", () => {
|
||||
it("all achievements start as locked", () => {
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["FirstSteps"].unlocked).toBe(false);
|
||||
expect(state.achievements["GrowingStrong"].unlocked).toBe(false);
|
||||
});
|
||||
|
||||
it("totalUnlocked starts at 0", () => {
|
||||
expect(get(achievementsStore).totalUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("lastUnlocked starts as null", () => {
|
||||
expect(get(achievementsStore).lastUnlocked).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived stores initial state", () => {
|
||||
it("unlockedAchievements is initially empty", () => {
|
||||
expect(get(unlockedAchievements)).toEqual([]);
|
||||
});
|
||||
|
||||
it("lockedAchievements contains all achievements initially", () => {
|
||||
const locked = get(lockedAchievements);
|
||||
const total = Object.keys(get(achievementsStore).achievements).length;
|
||||
expect(locked.length).toBe(total);
|
||||
});
|
||||
|
||||
it("achievementsByRarity groups achievements into rarity buckets", () => {
|
||||
const byRarity = get(achievementsByRarity);
|
||||
expect(byRarity.common).toBeInstanceOf(Array);
|
||||
expect(byRarity.rare).toBeInstanceOf(Array);
|
||||
expect(byRarity.epic).toBeInstanceOf(Array);
|
||||
expect(byRarity.legendary).toBeInstanceOf(Array);
|
||||
expect(byRarity.common.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("achievementProgress shows zero unlocked initially", () => {
|
||||
const progress = get(achievementProgress);
|
||||
expect(progress.unlocked).toBe(0);
|
||||
expect(progress.total).toBeGreaterThan(0);
|
||||
expect(progress.percentage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.unlockAchievement", () => {
|
||||
it("marks the achievement as unlocked and updates totalUnlocked", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("GrowingStrong"));
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["GrowingStrong"].unlocked).toBe(true);
|
||||
expect(state.totalUnlocked).toBe(1);
|
||||
expect(state.lastUnlocked?.id).toBe("GrowingStrong");
|
||||
});
|
||||
|
||||
it("sets unlockedAt from the event's unlocked_at timestamp", () => {
|
||||
achievementsStore.unlockAchievement({
|
||||
achievement: {
|
||||
id: "BlossomingCoder",
|
||||
name: "Blossoming Coder",
|
||||
description: "100k tokens",
|
||||
icon: "🌸",
|
||||
unlocked_at: "2026-01-15T12:00:00.000Z",
|
||||
},
|
||||
});
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["BlossomingCoder"].unlockedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("does nothing when the achievement is already unlocked", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenMaster"));
|
||||
const firstTotal = get(achievementsStore).totalUnlocked;
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenMaster"));
|
||||
expect(get(achievementsStore).totalUnlocked).toBe(firstTotal);
|
||||
});
|
||||
|
||||
it("calls playAchievementSound when playSound is true (default)", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenBillionaire"));
|
||||
expect(playAchievementSound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call playAchievementSound when playSound is false", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenTreasure"), false);
|
||||
expect(playAchievementSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs an error when playAchievementSound throws", () => {
|
||||
vi.mocked(playAchievementSound).mockImplementationOnce(() => {
|
||||
throw new Error("Sound failed");
|
||||
});
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
achievementsStore.unlockAchievement(makeEvent("HelloWorld"));
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to play achievement sound:", expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived stores after unlocks", () => {
|
||||
it("unlockedAchievements includes previously unlocked achievements", () => {
|
||||
const unlocked = get(unlockedAchievements);
|
||||
expect(unlocked.some((a) => a.id === "GrowingStrong")).toBe(true);
|
||||
});
|
||||
|
||||
it("lockedAchievements excludes previously unlocked achievements", () => {
|
||||
const locked = get(lockedAchievements);
|
||||
expect(locked.some((a) => a.id === "GrowingStrong")).toBe(false);
|
||||
});
|
||||
|
||||
it("achievementProgress reflects the current unlocked count", () => {
|
||||
const progress = get(achievementProgress);
|
||||
expect(progress.unlocked).toBeGreaterThan(0);
|
||||
expect(progress.percentage).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.updateProgress", () => {
|
||||
it("updates the progress value for an achievement", () => {
|
||||
achievementsStore.updateProgress("FirstMessage", 50);
|
||||
expect(get(achievementsStore).achievements["FirstMessage"].progress).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.reset", () => {
|
||||
it("resets totalUnlocked to 0 and lastUnlocked to null", () => {
|
||||
achievementsStore.reset();
|
||||
const state = get(achievementsStore);
|
||||
expect(state.totalUnlocked).toBe(0);
|
||||
expect(state.lastUnlocked).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("initAchievementsListener", () => {
|
||||
it("unlocks an achievement when the achievement:unlocked event fires", async () => {
|
||||
await initAchievementsListener();
|
||||
emitMockEvent("achievement:unlocked", makeEvent("FirstSteps"));
|
||||
expect(get(achievementsStore).achievements["FirstSteps"].unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("loads saved achievements from the backend without playing sounds", async () => {
|
||||
setMockInvokeResult("load_saved_achievements", [makeEvent("ConversationStarter")]);
|
||||
await initAchievementsListener();
|
||||
expect(get(achievementsStore).achievements["ConversationStarter"].unlocked).toBe(true);
|
||||
expect(playAchievementSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs an error when loading saved achievements fails", async () => {
|
||||
setMockInvokeResult("load_saved_achievements", new Error("Storage unavailable"));
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await initAchievementsListener();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to load saved achievements:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { emitMockEvent } from "../../../vitest.setup";
|
||||
import { debugConsoleStore, filteredLogs } from "./debugConsole";
|
||||
|
||||
describe("debugConsoleStore", () => {
|
||||
beforeEach(() => {
|
||||
debugConsoleStore.clear();
|
||||
debugConsoleStore.close();
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
debugConsoleStore.setAutoScroll(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
debugConsoleStore.restoreConsole();
|
||||
debugConsoleStore.clear();
|
||||
});
|
||||
|
||||
it("initializes with correct default state", () => {
|
||||
const state = get(debugConsoleStore);
|
||||
expect(state.logs).toEqual([]);
|
||||
expect(state.isOpen).toBe(false);
|
||||
expect(state.maxLogs).toBe(1000);
|
||||
expect(state.filterLevel).toBe("all");
|
||||
expect(state.autoScroll).toBe(true);
|
||||
});
|
||||
|
||||
describe("toggle", () => {
|
||||
it("opens when currently closed", () => {
|
||||
debugConsoleStore.toggle();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("closes when currently open", () => {
|
||||
debugConsoleStore.open();
|
||||
debugConsoleStore.toggle();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("open", () => {
|
||||
it("sets isOpen to true", () => {
|
||||
debugConsoleStore.open();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("close", () => {
|
||||
it("sets isOpen to false", () => {
|
||||
debugConsoleStore.open();
|
||||
debugConsoleStore.close();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("removes all log entries", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "info", message: "test entry" });
|
||||
expect(get(debugConsoleStore).logs.length).toBe(1);
|
||||
debugConsoleStore.clear();
|
||||
expect(get(debugConsoleStore).logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setFilterLevel", () => {
|
||||
it("updates filterLevel to the specified level", () => {
|
||||
debugConsoleStore.setFilterLevel("error");
|
||||
expect(get(debugConsoleStore).filterLevel).toBe("error");
|
||||
});
|
||||
|
||||
it("can reset filterLevel back to all", () => {
|
||||
debugConsoleStore.setFilterLevel("warn");
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
expect(get(debugConsoleStore).filterLevel).toBe("all");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAutoScroll", () => {
|
||||
it("disables autoScroll", () => {
|
||||
debugConsoleStore.setAutoScroll(false);
|
||||
expect(get(debugConsoleStore).autoScroll).toBe(false);
|
||||
});
|
||||
|
||||
it("re-enables autoScroll", () => {
|
||||
debugConsoleStore.setAutoScroll(false);
|
||||
debugConsoleStore.setAutoScroll(true);
|
||||
expect(get(debugConsoleStore).autoScroll).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupConsoleCapture", () => {
|
||||
afterEach(() => {
|
||||
debugConsoleStore.restoreConsole();
|
||||
});
|
||||
|
||||
it("captures console.log calls as info-level frontend logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.log("hello world");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "hello world");
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.level).toBe("info");
|
||||
expect(captured?.source).toBe("frontend");
|
||||
});
|
||||
|
||||
it("captures console.info calls as info-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.info("info message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "info message");
|
||||
expect(captured?.level).toBe("info");
|
||||
});
|
||||
|
||||
it("captures console.warn calls as warn-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.warn("warning message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "warning message");
|
||||
expect(captured?.level).toBe("warn");
|
||||
});
|
||||
|
||||
it("captures console.error calls as error-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.error("error message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "error message");
|
||||
expect(captured?.level).toBe("error");
|
||||
});
|
||||
|
||||
it("captures console.debug calls as debug-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.debug("debug message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "debug message");
|
||||
expect(captured?.level).toBe("debug");
|
||||
});
|
||||
|
||||
it("joins multiple console arguments with spaces", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.log("hello", "world", 42);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "hello world 42");
|
||||
expect(captured).toBeDefined();
|
||||
});
|
||||
|
||||
it("captures unhandled window error events", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
const errorEvent = new ErrorEvent("error", {
|
||||
message: "Test unhandled error",
|
||||
filename: "test.js",
|
||||
lineno: 10,
|
||||
colno: 5,
|
||||
});
|
||||
window.dispatchEvent(errorEvent);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find(
|
||||
(l) => l.level === "error" && l.message.includes("[Unhandled Error]")
|
||||
);
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.message).toContain("Test unhandled error");
|
||||
});
|
||||
|
||||
it("captures unhandled promise rejection events", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
const rejectionEvent = Object.assign(new Event("unhandledrejection"), {
|
||||
reason: "test rejection reason",
|
||||
});
|
||||
window.dispatchEvent(rejectionEvent);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find(
|
||||
(l) => l.level === "error" && l.message.includes("[Unhandled Promise Rejection]")
|
||||
);
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.message).toContain("test rejection reason");
|
||||
});
|
||||
});
|
||||
|
||||
describe("restoreConsole", () => {
|
||||
it("stops capturing console output after restore", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
debugConsoleStore.restoreConsole();
|
||||
const countBefore = get(debugConsoleStore).logs.length;
|
||||
console.log("this should not be captured");
|
||||
expect(get(debugConsoleStore).logs.length).toBe(countBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupBackendLogsListener", () => {
|
||||
it("captures backend logs emitted via debug:log event", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "info", message: "backend message" });
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].level).toBe("info");
|
||||
expect(logs[0].message).toBe("backend message");
|
||||
expect(logs[0].source).toBe("backend");
|
||||
});
|
||||
|
||||
it("handles different log levels from backend", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "error", message: "backend error" });
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs[0].level).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("circular buffer", () => {
|
||||
it("drops oldest log when exceeding 1000-entry limit", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
for (let i = 0; i < 1001; i++) {
|
||||
emitMockEvent("debug:log", { level: "info", message: `log ${i}` });
|
||||
}
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs.length).toBe(1000);
|
||||
expect(logs[0].message).toBe("log 1");
|
||||
expect(logs[999].message).toBe("log 1000");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filteredLogs derived store", () => {
|
||||
beforeEach(async () => {
|
||||
debugConsoleStore.clear();
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
debugConsoleStore.clear();
|
||||
});
|
||||
|
||||
it("returns all logs when filterLevel is all", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
expect(get(filteredLogs).length).toBe(4);
|
||||
});
|
||||
|
||||
it("returns only error logs when filterLevel is error", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("error");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].level).toBe("error");
|
||||
});
|
||||
|
||||
it("returns warn and error logs when filterLevel is warn", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("warn");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs.every((l) => l.level === "warn" || l.level === "error")).toBe(true);
|
||||
});
|
||||
|
||||
it("excludes debug logs when filterLevel is info", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("info");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(3);
|
||||
expect(logs.some((l) => l.level === "debug")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns all log levels when filterLevel is debug", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("debug");
|
||||
expect(get(filteredLogs).length).toBe(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user