generated from nhcarrigan/template
fa906684c2
## Summary - **fix**: `show_thinking_blocks` setting now persists across sessions — it was defined on the TypeScript side but missing from the Rust `HikariConfig` struct, so serde silently dropped it on every save/load - **feat**: Tool calls are now rendered as collapsible blocks matching the Extended Thinking block aesthetic, replacing the old inline dropdown approach - **feat**: Add configurable max output tokens setting - **feat**: Use random creative names for conversation tabs - **test**: Significantly expanded frontend unit test coverage - **docs**: Require tests for all changes in CLAUDE.md - **feat**: Allow users to specify a custom terminal font (Closes #176) - **feat**: Display friendly names for memory files derived from the first heading (Closes #177) - **feat**: Add custom UI font support for the app chrome (buttons, labels, tabs) - **fix**: Apply custom UI font to the full app interface — `.app-container` was hardcoded, blocking inheritance from `body`; also renamed "Custom Font" to "Custom Terminal Font" for clarity ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #175 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
293 lines
8.0 KiB
TypeScript
293 lines
8.0 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) {
|
|
const err = result;
|
|
return Promise.resolve().then(() => {
|
|
throw err;
|
|
});
|
|
}
|
|
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 "list_drafts":
|
|
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();
|
|
});
|