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.
288 lines
7.8 KiB
TypeScript
288 lines
7.8 KiB
TypeScript
import "@testing-library/jest-dom/vitest";
|
|
import { vi, beforeEach } from "vitest";
|
|
|
|
// Mock Tauri invoke API
|
|
const mockInvokeResults: Record<string, unknown> = {};
|
|
|
|
export function setMockInvokeResult(command: string, result: unknown) {
|
|
mockInvokeResults[command] = result;
|
|
}
|
|
|
|
export function clearMockInvokeResults() {
|
|
Object.keys(mockInvokeResults).forEach((key) => delete mockInvokeResults[key]);
|
|
}
|
|
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: vi.fn((command: string) => {
|
|
if (command in mockInvokeResults) {
|
|
const result = mockInvokeResults[command];
|
|
if (result instanceof Error) {
|
|
return Promise.reject(result);
|
|
}
|
|
return Promise.resolve(result);
|
|
}
|
|
// Default return values for common commands
|
|
switch (command) {
|
|
case "get_config":
|
|
return Promise.resolve({
|
|
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: {},
|
|
});
|
|
case "list_quick_actions":
|
|
return Promise.resolve([]);
|
|
case "list_snippets":
|
|
return Promise.resolve([]);
|
|
case "list_sessions":
|
|
return Promise.resolve([]);
|
|
case "get_usage_stats":
|
|
return Promise.resolve({
|
|
total_messages: 0,
|
|
total_sessions: 0,
|
|
total_tokens: 0,
|
|
total_cost: 0,
|
|
});
|
|
case "get_persisted_stats":
|
|
return Promise.resolve({
|
|
lifetime_messages: 0,
|
|
lifetime_sessions: 0,
|
|
lifetime_tokens: 0,
|
|
lifetime_cost: 0,
|
|
achievements: [],
|
|
unlocked_achievements: [],
|
|
});
|
|
case "load_saved_achievements":
|
|
return Promise.resolve([]);
|
|
case "list_clipboard_entries":
|
|
return Promise.resolve([]);
|
|
case "cleanup_temp_files":
|
|
return Promise.resolve();
|
|
case "validate_directory":
|
|
return Promise.resolve(true);
|
|
case "git_status":
|
|
return Promise.resolve({
|
|
branch: "main",
|
|
files: [],
|
|
ahead: 0,
|
|
behind: 0,
|
|
});
|
|
default:
|
|
return Promise.resolve(null);
|
|
}
|
|
}),
|
|
}));
|
|
|
|
// Mock Tauri event API
|
|
const eventListeners: Map<string, Array<(event: { payload: unknown }) => void>> = new Map();
|
|
|
|
export function emitMockEvent(eventName: string, payload: unknown) {
|
|
const listeners = eventListeners.get(eventName) || [];
|
|
listeners.forEach((listener) => listener({ payload }));
|
|
}
|
|
|
|
export function clearMockEventListeners() {
|
|
eventListeners.clear();
|
|
}
|
|
|
|
vi.mock("@tauri-apps/api/event", () => ({
|
|
listen: vi.fn((eventName: string, handler: (event: { payload: unknown }) => void) => {
|
|
const listeners = eventListeners.get(eventName) || [];
|
|
listeners.push(handler);
|
|
eventListeners.set(eventName, listeners);
|
|
// Return an unlisten function
|
|
return Promise.resolve(() => {
|
|
const currentListeners = eventListeners.get(eventName) || [];
|
|
const index = currentListeners.indexOf(handler);
|
|
if (index > -1) {
|
|
currentListeners.splice(index, 1);
|
|
}
|
|
});
|
|
}),
|
|
emit: vi.fn(),
|
|
}));
|
|
|
|
// Mock Tauri plugins
|
|
vi.mock("@tauri-apps/plugin-dialog", () => ({
|
|
save: vi.fn(() => Promise.resolve(null)),
|
|
open: vi.fn(() => Promise.resolve(null)),
|
|
message: vi.fn(() => Promise.resolve()),
|
|
ask: vi.fn(() => Promise.resolve(true)),
|
|
confirm: vi.fn(() => Promise.resolve(true)),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-fs", () => ({
|
|
writeTextFile: vi.fn(() => Promise.resolve()),
|
|
readTextFile: vi.fn(() => Promise.resolve("{}")),
|
|
exists: vi.fn(() => Promise.resolve(false)),
|
|
mkdir: vi.fn(() => Promise.resolve()),
|
|
remove: vi.fn(() => Promise.resolve()),
|
|
readDir: vi.fn(() => Promise.resolve([])),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-opener", () => ({
|
|
openPath: vi.fn(() => Promise.resolve()),
|
|
openUrl: vi.fn(() => Promise.resolve()),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-notification", () => ({
|
|
sendNotification: vi.fn(() => Promise.resolve()),
|
|
requestPermission: vi.fn(() => Promise.resolve("granted")),
|
|
isPermissionGranted: vi.fn(() => Promise.resolve(true)),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-clipboard-manager", () => ({
|
|
writeText: vi.fn(() => Promise.resolve()),
|
|
readText: vi.fn(() => Promise.resolve("")),
|
|
writeImage: vi.fn(() => Promise.resolve()),
|
|
readImage: vi.fn(() => Promise.resolve(null)),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-os", () => ({
|
|
platform: vi.fn(() => Promise.resolve("linux")),
|
|
arch: vi.fn(() => Promise.resolve("x86_64")),
|
|
type: vi.fn(() => Promise.resolve("Linux")),
|
|
version: vi.fn(() => Promise.resolve("1.0.0")),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/plugin-http", () => ({
|
|
fetch: vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({}),
|
|
text: () => Promise.resolve(""),
|
|
})
|
|
),
|
|
}));
|
|
|
|
// Mock browser APIs
|
|
class MockAudioElement {
|
|
src = "";
|
|
volume = 1;
|
|
loop = false;
|
|
currentTime = 0;
|
|
paused = true;
|
|
preload = "auto";
|
|
onloadeddata: (() => void) | null = null;
|
|
onended: (() => void) | null = null;
|
|
onerror: ((e: Event) => void) | null = null;
|
|
|
|
play() {
|
|
this.paused = false;
|
|
return Promise.resolve();
|
|
}
|
|
|
|
pause() {
|
|
this.paused = true;
|
|
}
|
|
|
|
load() {
|
|
if (this.onloadeddata) {
|
|
setTimeout(() => this.onloadeddata?.(), 0);
|
|
}
|
|
}
|
|
|
|
addEventListener(event: string, handler: () => void) {
|
|
if (event === "loadeddata") this.onloadeddata = handler;
|
|
if (event === "ended") this.onended = handler;
|
|
}
|
|
|
|
removeEventListener() {
|
|
// No-op for tests
|
|
}
|
|
}
|
|
|
|
// @ts-expect-error - Mock Audio constructor
|
|
globalThis.Audio = MockAudioElement;
|
|
|
|
// Mock localStorage
|
|
const localStorageStore: Record<string, string> = {};
|
|
|
|
Object.defineProperty(globalThis, "localStorage", {
|
|
value: {
|
|
getItem: vi.fn((key: string) => localStorageStore[key] || null),
|
|
setItem: vi.fn((key: string, value: string) => {
|
|
localStorageStore[key] = value;
|
|
}),
|
|
removeItem: vi.fn((key: string) => {
|
|
delete localStorageStore[key];
|
|
}),
|
|
clear: vi.fn(() => {
|
|
Object.keys(localStorageStore).forEach((key) => delete localStorageStore[key]);
|
|
}),
|
|
key: vi.fn((index: number) => Object.keys(localStorageStore)[index] || null),
|
|
get length() {
|
|
return Object.keys(localStorageStore).length;
|
|
},
|
|
},
|
|
writable: true,
|
|
});
|
|
|
|
// Mock matchMedia
|
|
Object.defineProperty(globalThis, "matchMedia", {
|
|
writable: true,
|
|
value: vi.fn((query: string) => ({
|
|
matches: query.includes("dark"),
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
});
|
|
|
|
// Mock ResizeObserver
|
|
class MockResizeObserver {
|
|
observe = vi.fn();
|
|
unobserve = vi.fn();
|
|
disconnect = vi.fn();
|
|
}
|
|
|
|
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
|
|
|
// Mock IntersectionObserver
|
|
class MockIntersectionObserver {
|
|
observe = vi.fn();
|
|
unobserve = vi.fn();
|
|
disconnect = vi.fn();
|
|
}
|
|
|
|
globalThis.IntersectionObserver =
|
|
MockIntersectionObserver as unknown as typeof IntersectionObserver;
|
|
|
|
// Mock requestAnimationFrame
|
|
globalThis.requestAnimationFrame = vi.fn((callback) => {
|
|
return setTimeout(callback, 0) as unknown as number;
|
|
});
|
|
|
|
globalThis.cancelAnimationFrame = vi.fn((id) => {
|
|
clearTimeout(id);
|
|
});
|
|
|
|
// Reset all mocks before each test
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
clearMockInvokeResults();
|
|
clearMockEventListeners();
|
|
});
|