generated from nhcarrigan/template
1d94bdfbb0
Implemented a confirmation modal when users try to close the application: - Modal always shows with three options: Cancel, Minimize to Tray, Close Application - Detects if Claude is actively running and shows appropriate warning message - Removed minimize_to_tray config setting (no longer needed) - Added core:window:allow-hide permission for window hiding - Created CloseAppConfirmModal component with keyboard shortcuts (Escape to cancel) - Added close_application command to properly exit the app - Backend emits window-close-requested event for frontend to handle This provides better UX by giving users clear choices every time they close, preventing accidental closures during active work sessions.
491 lines
17 KiB
TypeScript
491 lines
17 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import {
|
|
configStore,
|
|
maskPaths,
|
|
clampFontSize,
|
|
applyFontSize,
|
|
applyTheme,
|
|
applyCustomThemeColors,
|
|
clearCustomThemeColors,
|
|
MIN_FONT_SIZE,
|
|
MAX_FONT_SIZE,
|
|
DEFAULT_FONT_SIZE,
|
|
type HikariConfig,
|
|
type Theme,
|
|
type CustomThemeColors,
|
|
} from "./config";
|
|
|
|
// Mock Tauri APIs
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: vi.fn(),
|
|
}));
|
|
|
|
describe("config store", () => {
|
|
describe("font size constants", () => {
|
|
it("has correct MIN_FONT_SIZE", () => {
|
|
expect(MIN_FONT_SIZE).toBe(10);
|
|
});
|
|
|
|
it("has correct MAX_FONT_SIZE", () => {
|
|
expect(MAX_FONT_SIZE).toBe(24);
|
|
});
|
|
|
|
it("has correct DEFAULT_FONT_SIZE", () => {
|
|
expect(DEFAULT_FONT_SIZE).toBe(14);
|
|
});
|
|
});
|
|
|
|
describe("clampFontSize", () => {
|
|
it("returns the same value when within range", () => {
|
|
expect(clampFontSize(14)).toBe(14);
|
|
expect(clampFontSize(10)).toBe(10);
|
|
expect(clampFontSize(24)).toBe(24);
|
|
expect(clampFontSize(18)).toBe(18);
|
|
});
|
|
|
|
it("clamps values below minimum", () => {
|
|
expect(clampFontSize(5)).toBe(MIN_FONT_SIZE);
|
|
expect(clampFontSize(0)).toBe(MIN_FONT_SIZE);
|
|
expect(clampFontSize(-10)).toBe(MIN_FONT_SIZE);
|
|
expect(clampFontSize(9)).toBe(MIN_FONT_SIZE);
|
|
});
|
|
|
|
it("clamps values above maximum", () => {
|
|
expect(clampFontSize(30)).toBe(MAX_FONT_SIZE);
|
|
expect(clampFontSize(100)).toBe(MAX_FONT_SIZE);
|
|
expect(clampFontSize(25)).toBe(MAX_FONT_SIZE);
|
|
});
|
|
});
|
|
|
|
describe("maskPaths", () => {
|
|
it("returns text unchanged when hidePaths is false", () => {
|
|
const text = "/home/naomi/code/project/file.ts";
|
|
expect(maskPaths(text, false)).toBe(text);
|
|
});
|
|
|
|
it("masks Unix home paths", () => {
|
|
const text = "/home/naomi/code/project/file.ts";
|
|
expect(maskPaths(text, true)).toBe("/home/****/code/project/file.ts");
|
|
});
|
|
|
|
it("masks macOS user paths", () => {
|
|
const text = "/Users/naomi/Documents/project/file.ts";
|
|
expect(maskPaths(text, true)).toBe("/Users/****/Documents/project/file.ts");
|
|
});
|
|
|
|
it("masks Windows user paths", () => {
|
|
const text = "C:\\Users\\naomi\\Documents\\project\\file.ts";
|
|
expect(maskPaths(text, true)).toBe("C:\\Users\\****\\Documents\\project\\file.ts");
|
|
});
|
|
|
|
it("masks tilde paths", () => {
|
|
const text = "~/code/project/file.ts";
|
|
expect(maskPaths(text, true)).toBe("****/code/project/file.ts");
|
|
});
|
|
|
|
it("masks multiple paths in the same text", () => {
|
|
const text = "Editing /home/naomi/file1.ts and /home/naomi/file2.ts";
|
|
expect(maskPaths(text, true)).toBe("Editing /home/****/file1.ts and /home/****/file2.ts");
|
|
});
|
|
|
|
it("handles mixed path types", () => {
|
|
const text = "Unix: /home/user/file, Mac: /Users/user/file, Win: C:\\Users\\user\\file";
|
|
const expected = "Unix: /home/****/file, Mac: /Users/****/file, Win: C:\\Users\\****\\file";
|
|
expect(maskPaths(text, true)).toBe(expected);
|
|
});
|
|
|
|
it("handles paths with special characters in username", () => {
|
|
const text = "/home/user-name_123/project";
|
|
expect(maskPaths(text, true)).toBe("/home/****/project");
|
|
});
|
|
|
|
it("does not mask non-path text", () => {
|
|
const text = "This is just regular text without any paths";
|
|
expect(maskPaths(text, true)).toBe(text);
|
|
});
|
|
|
|
it("handles empty string", () => {
|
|
expect(maskPaths("", true)).toBe("");
|
|
expect(maskPaths("", false)).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("Theme type", () => {
|
|
it("accepts valid theme values", () => {
|
|
const themes: Theme[] = ["dark", "light", "high-contrast", "custom"];
|
|
themes.forEach((theme) => {
|
|
expect(["dark", "light", "high-contrast", "custom"]).toContain(theme);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CustomThemeColors interface", () => {
|
|
it("can create a valid custom theme colors object", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#1a1a2e",
|
|
bg_secondary: "#16213e",
|
|
bg_terminal: "#0f0f23",
|
|
accent_primary: "#e94560",
|
|
accent_secondary: "#533483",
|
|
text_primary: "#eaeaea",
|
|
text_secondary: "#a0a0a0",
|
|
border_color: "#333355",
|
|
};
|
|
|
|
expect(colors.bg_primary).toBe("#1a1a2e");
|
|
expect(colors.accent_primary).toBe("#e94560");
|
|
});
|
|
|
|
it("allows null values for optional colors", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: null,
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: "#e94560",
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
};
|
|
|
|
expect(colors.bg_primary).toBeNull();
|
|
expect(colors.accent_primary).toBe("#e94560");
|
|
});
|
|
});
|
|
|
|
describe("HikariConfig interface", () => {
|
|
it("can create a valid config object with all fields", () => {
|
|
const config: HikariConfig = {
|
|
model: "claude-sonnet-4",
|
|
api_key: "test-key",
|
|
custom_instructions: "Be helpful",
|
|
mcp_servers_json: "{}",
|
|
auto_granted_tools: ["Read", "Write"],
|
|
theme: "dark",
|
|
greeting_enabled: true,
|
|
greeting_custom_prompt: "Hello!",
|
|
notifications_enabled: true,
|
|
notification_volume: 0.7,
|
|
always_on_top: false,
|
|
update_checks_enabled: true,
|
|
character_panel_width: 300,
|
|
font_size: 14,
|
|
streamer_mode: false,
|
|
streamer_hide_paths: false,
|
|
compact_mode: false,
|
|
profile_name: "Naomi",
|
|
profile_avatar_path: "/path/to/avatar.png",
|
|
profile_bio: "Developer",
|
|
custom_theme_colors: {
|
|
bg_primary: null,
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: null,
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
},
|
|
budget_enabled: false,
|
|
session_token_budget: null,
|
|
session_cost_budget: null,
|
|
budget_action: "warn",
|
|
budget_warning_threshold: 0.8,
|
|
discord_rpc_enabled: true,
|
|
};
|
|
|
|
expect(config.model).toBe("claude-sonnet-4");
|
|
expect(config.auto_granted_tools).toEqual(["Read", "Write"]);
|
|
expect(config.theme).toBe("dark");
|
|
});
|
|
|
|
it("allows null values for optional fields", () => {
|
|
const config: HikariConfig = {
|
|
model: null,
|
|
api_key: null,
|
|
custom_instructions: null,
|
|
mcp_servers_json: null,
|
|
auto_granted_tools: [],
|
|
theme: "dark",
|
|
greeting_enabled: true,
|
|
greeting_custom_prompt: null,
|
|
notifications_enabled: true,
|
|
notification_volume: 0.7,
|
|
always_on_top: false,
|
|
update_checks_enabled: true,
|
|
character_panel_width: null,
|
|
font_size: 14,
|
|
streamer_mode: false,
|
|
streamer_hide_paths: false,
|
|
compact_mode: false,
|
|
profile_name: null,
|
|
profile_avatar_path: null,
|
|
profile_bio: null,
|
|
custom_theme_colors: {
|
|
bg_primary: null,
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: null,
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
},
|
|
budget_enabled: false,
|
|
session_token_budget: null,
|
|
session_cost_budget: null,
|
|
budget_action: "warn",
|
|
budget_warning_threshold: 0.8,
|
|
discord_rpc_enabled: true,
|
|
};
|
|
|
|
expect(config.model).toBeNull();
|
|
expect(config.api_key).toBeNull();
|
|
expect(config.character_panel_width).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("applyFontSize", () => {
|
|
beforeEach(() => {
|
|
// Reset document state
|
|
if (typeof document !== "undefined") {
|
|
document.documentElement.style.removeProperty("--terminal-font-size");
|
|
}
|
|
});
|
|
|
|
it("sets CSS variable for valid font size", () => {
|
|
applyFontSize(16);
|
|
const value = document.documentElement.style.getPropertyValue("--terminal-font-size");
|
|
expect(value).toBe("16px");
|
|
});
|
|
|
|
it("clamps font size below minimum", () => {
|
|
applyFontSize(5);
|
|
const value = document.documentElement.style.getPropertyValue("--terminal-font-size");
|
|
expect(value).toBe(`${MIN_FONT_SIZE}px`);
|
|
});
|
|
|
|
it("clamps font size above maximum", () => {
|
|
applyFontSize(50);
|
|
const value = document.documentElement.style.getPropertyValue("--terminal-font-size");
|
|
expect(value).toBe(`${MAX_FONT_SIZE}px`);
|
|
});
|
|
});
|
|
|
|
describe("applyTheme", () => {
|
|
beforeEach(() => {
|
|
// Reset document state
|
|
if (typeof document !== "undefined") {
|
|
document.documentElement.removeAttribute("data-theme");
|
|
clearCustomThemeColors();
|
|
}
|
|
});
|
|
|
|
it("sets data-theme attribute for dark theme", () => {
|
|
applyTheme("dark");
|
|
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
});
|
|
|
|
it("sets data-theme attribute for light theme", () => {
|
|
applyTheme("light");
|
|
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
|
|
});
|
|
|
|
it("sets data-theme attribute for high-contrast theme", () => {
|
|
applyTheme("high-contrast");
|
|
expect(document.documentElement.getAttribute("data-theme")).toBe("high-contrast");
|
|
});
|
|
|
|
it("uses dark as base for custom theme", () => {
|
|
applyTheme("custom");
|
|
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
|
|
});
|
|
|
|
it("applies custom colors when theme is custom", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#1a1a2e",
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: "#e94560",
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
};
|
|
|
|
applyTheme("custom", colors);
|
|
|
|
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("#1a1a2e");
|
|
expect(document.documentElement.style.getPropertyValue("--accent-primary")).toBe("#e94560");
|
|
});
|
|
|
|
it("does not apply custom colors for non-custom themes", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#1a1a2e",
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: null,
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
};
|
|
|
|
applyTheme("dark", colors);
|
|
|
|
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("applyCustomThemeColors", () => {
|
|
beforeEach(() => {
|
|
clearCustomThemeColors();
|
|
});
|
|
|
|
it("applies all provided colors", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#111111",
|
|
bg_secondary: "#222222",
|
|
bg_terminal: "#333333",
|
|
accent_primary: "#444444",
|
|
accent_secondary: "#555555",
|
|
text_primary: "#666666",
|
|
text_secondary: "#777777",
|
|
border_color: "#888888",
|
|
};
|
|
|
|
applyCustomThemeColors(colors);
|
|
|
|
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("#111111");
|
|
expect(document.documentElement.style.getPropertyValue("--bg-secondary")).toBe("#222222");
|
|
expect(document.documentElement.style.getPropertyValue("--bg-terminal")).toBe("#333333");
|
|
expect(document.documentElement.style.getPropertyValue("--accent-primary")).toBe("#444444");
|
|
expect(document.documentElement.style.getPropertyValue("--accent-secondary")).toBe("#555555");
|
|
expect(document.documentElement.style.getPropertyValue("--text-primary")).toBe("#666666");
|
|
expect(document.documentElement.style.getPropertyValue("--text-secondary")).toBe("#777777");
|
|
expect(document.documentElement.style.getPropertyValue("--border-color")).toBe("#888888");
|
|
});
|
|
|
|
it("skips null values", () => {
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#111111",
|
|
bg_secondary: null,
|
|
bg_terminal: null,
|
|
accent_primary: null,
|
|
accent_secondary: null,
|
|
text_primary: null,
|
|
text_secondary: null,
|
|
border_color: null,
|
|
};
|
|
|
|
applyCustomThemeColors(colors);
|
|
|
|
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("#111111");
|
|
expect(document.documentElement.style.getPropertyValue("--bg-secondary")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("clearCustomThemeColors", () => {
|
|
it("removes all custom theme CSS properties", () => {
|
|
// First apply some colors
|
|
const colors: CustomThemeColors = {
|
|
bg_primary: "#111111",
|
|
bg_secondary: "#222222",
|
|
bg_terminal: "#333333",
|
|
accent_primary: "#444444",
|
|
accent_secondary: "#555555",
|
|
text_primary: "#666666",
|
|
text_secondary: "#777777",
|
|
border_color: "#888888",
|
|
};
|
|
applyCustomThemeColors(colors);
|
|
|
|
// Then clear them
|
|
clearCustomThemeColors();
|
|
|
|
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--bg-secondary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--bg-terminal")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--accent-primary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--accent-secondary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--text-primary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--text-secondary")).toBe("");
|
|
expect(document.documentElement.style.getPropertyValue("--border-color")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("derived stores", () => {
|
|
// Note: These tests verify the derived store logic by testing the derivation functions
|
|
// The actual stores depend on configStore which requires Tauri invoke mocking
|
|
|
|
it("isDarkTheme returns true for dark theme config", () => {
|
|
// Test the derivation logic
|
|
const darkConfig = { theme: "dark" as Theme };
|
|
expect(darkConfig.theme === "dark").toBe(true);
|
|
});
|
|
|
|
it("isDarkTheme returns false for light theme config", () => {
|
|
const lightConfig = { theme: "light" as Theme };
|
|
expect(lightConfig.theme === "dark").toBe(false);
|
|
});
|
|
|
|
it("isStreamerMode derives from streamer_mode config", () => {
|
|
const configWithStreamerMode = { streamer_mode: true };
|
|
const configWithoutStreamerMode = { streamer_mode: false };
|
|
|
|
expect(configWithStreamerMode.streamer_mode).toBe(true);
|
|
expect(configWithoutStreamerMode.streamer_mode).toBe(false);
|
|
});
|
|
|
|
it("isCompactMode derives from compact_mode config", () => {
|
|
const configWithCompactMode = { compact_mode: true };
|
|
const configWithoutCompactMode = { compact_mode: false };
|
|
|
|
expect(configWithCompactMode.compact_mode).toBe(true);
|
|
expect(configWithoutCompactMode.compact_mode).toBe(false);
|
|
});
|
|
|
|
it("shouldHidePaths requires both streamer_mode and streamer_hide_paths", () => {
|
|
const config1 = { streamer_mode: true, streamer_hide_paths: true };
|
|
const config2 = { streamer_mode: true, streamer_hide_paths: false };
|
|
const config3 = { streamer_mode: false, streamer_hide_paths: true };
|
|
const config4 = { streamer_mode: false, streamer_hide_paths: false };
|
|
|
|
expect(config1.streamer_mode && config1.streamer_hide_paths).toBe(true);
|
|
expect(config2.streamer_mode && config2.streamer_hide_paths).toBe(false);
|
|
expect(config3.streamer_mode && config3.streamer_hide_paths).toBe(false);
|
|
expect(config4.streamer_mode && config4.streamer_hide_paths).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("configStore methods", () => {
|
|
it("has all expected methods", () => {
|
|
expect(typeof configStore.loadConfig).toBe("function");
|
|
expect(typeof configStore.saveConfig).toBe("function");
|
|
expect(typeof configStore.updateConfig).toBe("function");
|
|
expect(typeof configStore.openSidebar).toBe("function");
|
|
expect(typeof configStore.closeSidebar).toBe("function");
|
|
expect(typeof configStore.toggleSidebar).toBe("function");
|
|
expect(typeof configStore.setTheme).toBe("function");
|
|
expect(typeof configStore.setCustomThemeColors).toBe("function");
|
|
expect(typeof configStore.setFontSize).toBe("function");
|
|
expect(typeof configStore.increaseFontSize).toBe("function");
|
|
expect(typeof configStore.decreaseFontSize).toBe("function");
|
|
expect(typeof configStore.resetFontSize).toBe("function");
|
|
expect(typeof configStore.addAutoGrantedTool).toBe("function");
|
|
expect(typeof configStore.removeAutoGrantedTool).toBe("function");
|
|
expect(typeof configStore.getConfig).toBe("function");
|
|
expect(typeof configStore.toggleStreamerMode).toBe("function");
|
|
expect(typeof configStore.toggleCompactMode).toBe("function");
|
|
expect(typeof configStore.setCompactMode).toBe("function");
|
|
});
|
|
|
|
it("has subscribable stores", () => {
|
|
expect(typeof configStore.config.subscribe).toBe("function");
|
|
expect(typeof configStore.isLoading.subscribe).toBe("function");
|
|
expect(typeof configStore.isSidebarOpen.subscribe).toBe("function");
|
|
expect(typeof configStore.saveError.subscribe).toBe("function");
|
|
});
|
|
});
|
|
});
|