Files
tolaria/src/App.test.tsx

274 lines
8.7 KiB
TypeScript
Raw Normal View History

import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64) * feat: add search backend (qmd CLI) and design file - Add search.rs: qmd integration for keyword (BM25), semantic (vsearch), and hybrid search modes via CLI JSON output - Register search_vault Tauri command in lib.rs - Design file with 3 frames: empty, results, no-results states - Fix pre-existing test failures: install @tauri-apps/plugin-opener, add mock in test setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add SearchPanel UI with Cmd+Shift+F shortcut - SearchPanel component: full-text search overlay with keyword/semantic mode toggle, debounced search (200ms), arrow key navigation, result count + elapsed time display, note type badges from vault entries - Cmd+Shift+F shortcut registered in useAppKeyboard - Mock search_vault handler in mock-tauri for browser dev mode - SearchResult/SearchResponse types in types.ts - Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut - scrollIntoView mock in test setup for jsdom compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct SearchPanel imports for Tauri/browser dual-mode Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri with a searchCall wrapper, fixing the TypeScript build error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make test_detect_collection_fallback robust when qmd not installed * fix: reset localStorage between App tests to prevent view mode state leak --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
// Provide a localStorage mock that supports all methods (jsdom's may be incomplete)
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} },
}
})()
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
// Mock @tauri-apps/api/core before importing App
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}))
// Mock mock-tauri module
const mockEntries = [
{
path: '/vault/project/test.md',
filename: 'test.md',
title: 'Test Project',
isA: 'Project',
aliases: [],
belongsTo: [],
relatedTo: [],
status: 'Active',
owner: 'Luca',
cadence: null,
modifiedAt: 1700000000,
createdAt: null,
fileSize: 1024,
template: null, sort: null,
outgoingLinks: [],
},
{
path: '/vault/topic/dev.md',
filename: 'dev.md',
title: 'Software Development',
isA: 'Topic',
aliases: ['Dev'],
belongsTo: [],
relatedTo: [],
status: null,
owner: null,
cadence: null,
modifiedAt: 1700000000,
createdAt: null,
fileSize: 256,
template: null, sort: null,
outgoingLinks: [],
},
]
const mockAllContent: Record<string, string> = {
'/vault/project/test.md': '---\ntitle: Test Project\nis_a: Project\n---\n\n# Test Project\n\nSome content.',
'/vault/topic/dev.md': '---\ntitle: Software Development\nis_a: Topic\n---\n\n# Software Development\n',
}
const mockCommandResults: Record<string, unknown> = {
list_vault: mockEntries,
list_vault_folders: [],
list_views: [],
get_all_content: mockAllContent,
get_modified_files: [],
get_note_content: mockAllContent['/vault/project/test.md'] || '',
get_file_history: [],
get_settings: { auto_pull_interval_minutes: null, telemetry_consent: true, crash_reporting_enabled: null, analytics_enabled: null, anonymous_id: null, release_channel: null },
git_pull: { status: 'up_to_date', message: 'Already up to date', updatedFiles: [], conflictFiles: [] },
save_settings: null,
check_vault_exists: true,
get_default_vault_path: '/Users/mock/Documents/Getting Started',
list_themes: [],
get_vault_settings: { theme: null },
}
vi.mock('./mock-tauri', () => ({
isTauri: () => false,
mockInvoke: vi.fn(async (cmd: string) => mockCommandResults[cmd] ?? null),
addMockEntry: vi.fn(),
updateMockContent: vi.fn(),
}))
feat: replace Anthropic API with Claude CLI for AI chat and agent panels (#159) * feat: add claude CLI subprocess backend for AI panels Add claude_cli.rs module that spawns the local `claude` CLI as a subprocess instead of calling the Anthropic API directly. This removes the need for users to configure an API key — the CLI uses the user's existing Claude authentication. - find_claude_binary(): discovers claude in PATH or common locations - check_cli(): returns install/version status for the frontend - run_chat_stream(): spawns claude -p with stream-json for chat panel - run_agent_stream(): spawns claude with MCP vault tools for agent panel - Parses stream-json NDJSON events and emits typed Tauri events - Remove http:default capability (no longer calling APIs from frontend) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace Anthropic API with Claude CLI for AI chat and agent panels Remove direct Anthropic API calls and the entire tool-use loop from the frontend. Both AI Chat and AI Agent panels now delegate to the `claude` CLI subprocess via Tauri commands, streaming NDJSON events back to the UI. Key changes: - ai-chat.ts / ai-agent.ts: replace SSE/API streaming with Tauri invoke + listen - useAIChat / useAiAgent hooks: simplified to use CLI streaming callbacks - AIChatPanel / AiPanel: remove API key dialogs, model selectors, undo support - SettingsPanel: remove Anthropic key field (no longer needed) - claude_cli.rs: add comprehensive tests (37 total, 90% coverage) - mock-handlers: replace ai_chat with check_claude_cli / stream_claude_* stubs - Net -432 lines across 17 files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add --verbose flag to claude CLI subprocess args * chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup) --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:49:58 +01:00
// Mock ai-chat utilities
vi.mock('./utils/ai-chat', () => ({
buildSystemPrompt: vi.fn(() => ({ prompt: '', totalTokens: 0, truncated: false })),
feat: replace Anthropic API with Claude CLI for AI chat and agent panels (#159) * feat: add claude CLI subprocess backend for AI panels Add claude_cli.rs module that spawns the local `claude` CLI as a subprocess instead of calling the Anthropic API directly. This removes the need for users to configure an API key — the CLI uses the user's existing Claude authentication. - find_claude_binary(): discovers claude in PATH or common locations - check_cli(): returns install/version status for the frontend - run_chat_stream(): spawns claude -p with stream-json for chat panel - run_agent_stream(): spawns claude with MCP vault tools for agent panel - Parses stream-json NDJSON events and emits typed Tauri events - Remove http:default capability (no longer calling APIs from frontend) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace Anthropic API with Claude CLI for AI chat and agent panels Remove direct Anthropic API calls and the entire tool-use loop from the frontend. Both AI Chat and AI Agent panels now delegate to the `claude` CLI subprocess via Tauri commands, streaming NDJSON events back to the UI. Key changes: - ai-chat.ts / ai-agent.ts: replace SSE/API streaming with Tauri invoke + listen - useAIChat / useAiAgent hooks: simplified to use CLI streaming callbacks - AIChatPanel / AiPanel: remove API key dialogs, model selectors, undo support - SettingsPanel: remove Anthropic key field (no longer needed) - claude_cli.rs: add comprehensive tests (37 total, 90% coverage) - mock-handlers: replace ai_chat with check_claude_cli / stream_claude_* stubs - Net -432 lines across 17 files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add --verbose flag to claude CLI subprocess args * chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup) --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:49:58 +01:00
checkClaudeCli: vi.fn(async () => ({ installed: false })),
streamClaudeChat: vi.fn(async () => 'mock-session'),
}))
// Mock BlockNote components (they need DOM APIs not available in jsdom)
vi.mock('@blocknote/core', () => ({
BlockNoteSchema: { create: () => ({}) },
defaultInlineContentSpecs: {},
filterSuggestionItems: vi.fn(() => []),
}))
vi.mock('@blocknote/core/extensions', () => ({
filterSuggestionItems: vi.fn(() => []),
}))
vi.mock('@blocknote/react', () => ({
createReactInlineContentSpec: () => ({ render: () => null }),
useCreateBlockNote: () => ({
tryParseMarkdownToBlocks: async () => [],
replaceBlocks: () => {},
document: [],
insertInlineContent: () => {},
onMount: (cb: () => void) => { cb(); return () => {} },
}),
SuggestionMenuController: () => null,
}))
vi.mock('@blocknote/mantine', () => ({
BlockNoteView: ({ children }: { children?: React.ReactNode }) => <div data-testid="blocknote-view">{children}</div>,
}))
vi.mock('@blocknote/mantine/style.css', () => ({}))
import App from './App'
describe('App', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset view mode and onboarding state between tests
localStorage.removeItem('tolaria-view-mode')
localStorage.removeItem('tolaria-view-mode')
feat: full-text search with qmd backend and SearchPanel UI (Cmd+Shift+F) (#64) * feat: add search backend (qmd CLI) and design file - Add search.rs: qmd integration for keyword (BM25), semantic (vsearch), and hybrid search modes via CLI JSON output - Register search_vault Tauri command in lib.rs - Design file with 3 frames: empty, results, no-results states - Fix pre-existing test failures: install @tauri-apps/plugin-opener, add mock in test setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add SearchPanel UI with Cmd+Shift+F shortcut - SearchPanel component: full-text search overlay with keyword/semantic mode toggle, debounced search (200ms), arrow key navigation, result count + elapsed time display, note type badges from vault entries - Cmd+Shift+F shortcut registered in useAppKeyboard - Mock search_vault handler in mock-tauri for browser dev mode - SearchResult/SearchResponse types in types.ts - Tests: 15 new tests for SearchPanel + 2 for keyboard shortcut - scrollIntoView mock in test setup for jsdom compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct SearchPanel imports for Tauri/browser dual-mode Use invoke from @tauri-apps/api/core and mockInvoke from mock-tauri with a searchCall wrapper, fixing the TypeScript build error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make test_detect_collection_fallback robust when qmd not installed * fix: reset localStorage between App tests to prevent view mode state leak --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:09:50 +01:00
localStorage.removeItem('laputa-view-mode')
localStorage.removeItem('tolaria_welcome_dismissed')
localStorage.removeItem('laputa_welcome_dismissed')
})
it('renders the four-panel layout', async () => {
render(<App />)
// Wait for vault to load
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
})
it('loads and displays vault entries in sidebar', async () => {
render(<App />)
await waitFor(() => {
// Entries appear in both Sidebar and NoteList
expect(screen.getAllByText('Test Project').length).toBeGreaterThan(0)
expect(screen.getAllByText('Software Development').length).toBeGreaterThan(0)
})
})
it('shows empty state in editor when no note is selected', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('Select a note to start editing')).toBeInTheDocument()
})
})
it('shows keyboard shortcut hints', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText(/Cmd\+P to search/)).toBeInTheDocument()
})
})
it('registers keyboard shortcuts without error', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
// Cmd+S with no pending changes shows "Nothing to save"
fireEvent.keyDown(window, { key: 's', metaKey: true })
await waitFor(() => {
expect(screen.getByText('Nothing to save')).toBeInTheDocument()
})
})
it('renders sidebar with correct default selection (All Notes)', async () => {
render(<App />)
await waitFor(() => {
// "All Notes" should be rendered as the selected nav item
expect(screen.getByText('All Notes')).toBeInTheDocument()
expect(screen.getByText('Archive')).toBeInTheDocument()
})
})
it('defaults to All Notes when explicit organization is disabled in vault config', async () => {
const disabledWorkflowConfig = JSON.stringify({
zoom: null,
view_mode: null,
editor_mode: null,
tag_colors: null,
status_colors: null,
property_display_modes: null,
inbox: { noteListProperties: null, explicitOrganization: false },
})
localStorage.setItem('laputa:vault-config:/Users/mock/Documents/Getting Started', disabledWorkflowConfig)
localStorage.setItem('laputa:vault-config:/Volumes/Jupiter/Workspace/laputa-app/demo-vault-v2', disabledWorkflowConfig)
render(<App />)
await waitFor(() => {
expect(screen.queryByText('Inbox')).not.toBeInTheDocument()
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
})
it('renders status bar', async () => {
render(<App />)
// StatusBar should be present
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
// The status bar element should exist in the DOM
const appShell = document.querySelector('.app-shell')
expect(appShell).toBeInTheDocument()
})
it('Cmd+1 hides sidebar and note list (editor-only mode)', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
// All panels visible by default
expect(document.querySelector('.app__sidebar')).toBeInTheDocument()
expect(document.querySelector('.app__note-list')).toBeInTheDocument()
// Cmd+1 → editor-only
fireEvent.keyDown(window, { key: '1', metaKey: true })
await waitFor(() => {
expect(document.querySelector('.app__sidebar')).not.toBeInTheDocument()
expect(document.querySelector('.app__note-list')).not.toBeInTheDocument()
})
})
it('Cmd+2 shows editor + note list (sidebar hidden)', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
fireEvent.keyDown(window, { key: '2', metaKey: true })
await waitFor(() => {
expect(document.querySelector('.app__sidebar')).not.toBeInTheDocument()
expect(document.querySelector('.app__note-list')).toBeInTheDocument()
})
})
it('Cmd+3 restores all panels after Cmd+1', async () => {
render(<App />)
await waitFor(() => {
expect(screen.getByText('All Notes')).toBeInTheDocument()
})
// Switch to editor-only first
fireEvent.keyDown(window, { key: '1', metaKey: true })
await waitFor(() => {
expect(document.querySelector('.app__sidebar')).not.toBeInTheDocument()
})
// Cmd+3 → all panels
fireEvent.keyDown(window, { key: '3', metaKey: true })
await waitFor(() => {
expect(document.querySelector('.app__sidebar')).toBeInTheDocument()
expect(document.querySelector('.app__note-list')).toBeInTheDocument()
})
})
})