Files
tolaria/src/App.note-window-properties.test.tsx
2026-05-14 05:05:33 +02:00

274 lines
7.6 KiB
TypeScript

import { render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ReactNode } from 'react'
import type { Settings, VaultEntry } from './types'
import { DEFAULT_VAULTS } from './hooks/useVaultSwitcher'
import { TooltipProvider } from './components/ui/tooltip'
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
clear: () => { store = {} },
getItem: (key: string) => store[key] ?? null,
removeItem: (key: string) => { delete store[key] },
setItem: (key: string, value: string) => { store[key] = value },
}
})()
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
addEventListener: vi.fn(),
addListener: vi.fn(),
dispatchEvent: vi.fn(),
matches: false,
media: query,
onchange: null,
removeEventListener: vi.fn(),
removeListener: vi.fn(),
})),
})
const editorSnapshots = vi.hoisted(() => [] as Array<{
activeTabPath: string | null
entryTitles: string[]
}>)
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}))
vi.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: () => ({
innerSize: vi.fn(async () => ({ toLogical: () => ({ width: 1400, height: 900 }) })),
scaleFactor: vi.fn(async () => 1),
setMinSize: vi.fn(async () => {}),
setSize: vi.fn(async () => {}),
}),
}))
vi.mock('./components/Editor', () => ({
Editor: (props: { activeTabPath: string | null; entries: VaultEntry[] }) => {
const entryTitles = props.entries.map((entry) => entry.title)
editorSnapshots.push({ activeTabPath: props.activeTabPath, entryTitles })
return <div data-testid="mock-editor-entry-titles">{entryTitles.join('|')}</div>
},
}))
vi.mock('./hooks/useUpdater', () => ({
restartApp: vi.fn(),
useUpdater: () => ({
status: { state: 'idle' },
actions: {
checkForUpdates: vi.fn(async () => ({ kind: 'up-to-date' })),
dismiss: vi.fn(),
openReleaseNotes: vi.fn(),
startDownload: vi.fn(),
},
}),
}))
vi.mock('./utils/ai-chat', async () => {
const actual = await vi.importActual<typeof import('./utils/ai-chat')>('./utils/ai-chat')
return {
...actual,
buildSystemPrompt: vi.fn(() => ({ prompt: '', totalTokens: 0, truncated: false })),
checkClaudeCli: vi.fn(async () => ({ installed: false })),
streamClaudeChat: vi.fn(async () => 'mock-session'),
}
})
vi.mock('./utils/streamAiAgent', () => ({
streamAiAgent: vi.fn(async () => {}),
}))
function makeEntry(overrides: Partial<VaultEntry>): VaultEntry {
return {
path: '/vault/note.md',
filename: 'note.md',
title: 'Note',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: 1_700_000_000,
createdAt: null,
fileSize: 256,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
sidebarLabel: null,
template: null,
sort: null,
view: null,
visible: true,
organized: false,
favorite: false,
favoriteIndex: null,
listPropertiesDisplay: [],
outgoingLinks: [],
properties: {},
hasH1: true,
fileKind: 'markdown',
...overrides,
}
}
const activeEntry = makeEntry({
path: '/vault/project/test.md',
filename: 'test.md',
title: 'Test Project',
isA: 'Project',
})
const relatedEntry = makeEntry({
path: '/vault/topic/dev.md',
filename: 'dev.md',
title: 'Software Development',
isA: 'Topic',
})
const secondEntry = makeEntry({
path: '/vault/project/second.md',
filename: 'second.md',
title: 'Second Project',
isA: 'Project',
})
const entries = [activeEntry, relatedEntry, secondEntry]
const noteContent = '---\ntitle: Test Project\ntype: Project\n---\n\n# Test Project\n'
const defaultVaultPath = DEFAULT_VAULTS[0].path || '/Users/mock/Documents/Getting Started'
function createSettings(): Settings {
return {
anonymous_id: null,
analytics_enabled: null,
auto_pull_interval_minutes: null,
autogit_enabled: false,
autogit_idle_threshold_seconds: 90,
autogit_inactive_threshold_seconds: 30,
crash_reporting_enabled: null,
release_channel: null,
telemetry_consent: true,
}
}
const commandResults: Record<string, unknown> = {}
function resetCommandResults() {
Object.assign(commandResults, {
check_vault_exists: true,
get_all_content: { [activeEntry.path]: noteContent },
get_default_vault_path: defaultVaultPath,
get_file_history: [],
get_modified_files: [],
get_note_content: vi.fn(() => noteContent),
get_settings: createSettings(),
get_vault_settings: { theme: null },
is_git_repo: true,
list_themes: [],
list_vault: vi.fn(() => entries),
list_vault_folders: [],
list_views: [],
load_vault_list: {
active_vault: '/vault',
hidden_defaults: [],
vaults: [{ label: 'Test Vault', path: '/vault' }],
},
reload_vault_entry: vi.fn(({ path }: { path: string }) =>
entries.find((entry) => entry.path === path) ?? null,
),
save_settings: null,
sync_vault_asset_scope_for_window: null,
})
}
function resolveCommandResult(command: string, args?: unknown) {
const result = Reflect.get(commandResults, command) as unknown
return typeof result === 'function'
? (result as (input?: unknown) => unknown)(args)
: result ?? null
}
vi.mock('./mock-tauri', () => ({
addMockEntry: vi.fn(),
isTauri: vi.fn(() => false),
mockInvoke: vi.fn(async (command: string, args?: unknown) => resolveCommandResult(command, args)),
trackMockChange: vi.fn(),
updateMockContent: vi.fn(),
}))
import App from './App'
function renderApp(children: ReactNode) {
return render(<TooltipProvider>{children}</TooltipProvider>)
}
describe('App note windows', () => {
beforeEach(() => {
vi.clearAllMocks()
editorSnapshots.length = 0
resetCommandResults()
localStorage.clear()
localStorage.setItem('tolaria:claude-code-onboarding-dismissed', '1')
window.history.replaceState(
{},
'',
'/?window=note&path=%2Fvault%2Fproject%2Ftest.md&vault=%2Fvault&title=Test+Project',
)
})
it('keeps note windows scoped to the active entry without loading the vault index', async () => {
renderApp(<App />)
await waitFor(() => {
expect(commandResults.reload_vault_entry).toHaveBeenCalledWith({
path: activeEntry.path,
vaultPath: '/vault',
})
})
expect(commandResults.list_vault).not.toHaveBeenCalled()
await waitFor(() => {
expect(screen.getByTestId('mock-editor-entry-titles')).toHaveTextContent(
'Test Project',
)
})
expect(editorSnapshots.at(-1)).toEqual({
activeTabPath: activeEntry.path,
entryTitles: ['Test Project'],
})
})
it('opens repeated note windows without repeated full-vault scans', async () => {
const firstWindow = renderApp(<App />)
await waitFor(() => {
expect(commandResults.reload_vault_entry).toHaveBeenCalledWith({
path: activeEntry.path,
vaultPath: '/vault',
})
})
firstWindow.unmount()
window.history.replaceState(
{},
'',
'/?window=note&path=%2Fvault%2Fproject%2Fsecond.md&vault=%2Fvault&title=Second+Project',
)
renderApp(<App />)
await waitFor(() => {
expect(commandResults.reload_vault_entry).toHaveBeenCalledWith({
path: secondEntry.path,
vaultPath: '/vault',
})
})
expect(commandResults.reload_vault_entry).toHaveBeenCalledTimes(2)
expect(commandResults.list_vault).not.toHaveBeenCalled()
})
})