1338 lines
43 KiB
TypeScript
1338 lines
43 KiB
TypeScript
import { act, render, screen, fireEvent, waitFor, within } from '@testing-library/react'
|
|
import type { ReactNode } from 'react'
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { DEFAULT_VAULTS } from './hooks/useVaultSwitcher'
|
|
import { formatShortcutDisplay } from './hooks/appCommandCatalog'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import type { Settings, ViewDefinition, ViewFile } from './types'
|
|
|
|
// 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 })
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
})
|
|
|
|
// Mock @tauri-apps/api/core before importing App
|
|
vi.mock('@tauri-apps/api/core', () => ({
|
|
invoke: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@tauri-apps/api/window', async () => {
|
|
const actual = await vi.importActual<typeof import('@tauri-apps/api/window')>('@tauri-apps/api/window')
|
|
|
|
return {
|
|
...actual,
|
|
getCurrentWindow: () => ({
|
|
innerSize: vi.fn(async () => ({ toLogical: () => ({ width: 1400, height: 900 }) })),
|
|
scaleFactor: vi.fn(async () => 1),
|
|
setMinSize: vi.fn(async () => {}),
|
|
setSize: vi.fn(async () => {}),
|
|
}),
|
|
}
|
|
})
|
|
|
|
// Mock mock-tauri module
|
|
const mockEntries = [
|
|
{
|
|
path: '/vault/project/test.md',
|
|
filename: 'test.md',
|
|
title: 'Test Project',
|
|
isA: 'Project',
|
|
aliases: [],
|
|
belongsTo: [],
|
|
relatedTo: [],
|
|
status: 'Active',
|
|
archived: false,
|
|
owner: 'Luca',
|
|
cadence: null,
|
|
modifiedAt: 1700000000,
|
|
createdAt: null,
|
|
fileSize: 1024,
|
|
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',
|
|
},
|
|
{
|
|
path: '/vault/topic/dev.md',
|
|
filename: 'dev.md',
|
|
title: 'Software Development',
|
|
isA: 'Topic',
|
|
aliases: ['Dev'],
|
|
belongsTo: [],
|
|
relatedTo: [],
|
|
status: null,
|
|
archived: false,
|
|
owner: null,
|
|
cadence: null,
|
|
modifiedAt: 1700000000,
|
|
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',
|
|
},
|
|
]
|
|
|
|
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 mockVaultList = {
|
|
vaults: [{ label: 'Test Vault', path: '/vault' }],
|
|
active_vault: '/vault',
|
|
hidden_defaults: [],
|
|
}
|
|
|
|
const mockDefaultVaultPath = '/Users/mock/Documents/Getting Started'
|
|
const expectedDefaultVaultPath = DEFAULT_VAULTS[0].path || mockDefaultVaultPath
|
|
|
|
const mockCommandResults: Record<string, unknown> = {
|
|
load_vault_list: mockVaultList,
|
|
list_vault: mockEntries,
|
|
list_vault_folders: [],
|
|
list_views: [],
|
|
get_all_content: mockAllContent,
|
|
get_modified_files: [],
|
|
get_note_content: mockAllContent['/vault/project/test.md'] || '',
|
|
save_note_content: null,
|
|
reload_vault_entry: ({ path }: { path: string }) => mockEntries.find((entry) => entry.path === path) ?? null,
|
|
sync_vault_asset_scope_for_window: null,
|
|
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 },
|
|
is_git_repo: true,
|
|
init_git_repo: 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: expectedDefaultVaultPath,
|
|
list_themes: [],
|
|
get_vault_settings: { theme: null },
|
|
}
|
|
|
|
function buildNeighborhoodEntry({
|
|
path,
|
|
title,
|
|
relatedRefs,
|
|
outgoingLinks,
|
|
modifiedAt,
|
|
}: {
|
|
path: string
|
|
title: string
|
|
relatedRefs: string[]
|
|
outgoingLinks: string[]
|
|
modifiedAt: number
|
|
}) {
|
|
return {
|
|
path,
|
|
filename: path.split('/').pop() ?? `${title.toLowerCase()}.md`,
|
|
title,
|
|
isA: 'Note',
|
|
aliases: [],
|
|
belongsTo: [],
|
|
relatedTo: relatedRefs,
|
|
status: null,
|
|
modifiedAt,
|
|
createdAt: null,
|
|
fileSize: 128,
|
|
archived: false,
|
|
snippet: '',
|
|
wordCount: 12,
|
|
relationships: relatedRefs.length > 0 ? { 'Related to': relatedRefs } : {},
|
|
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',
|
|
}
|
|
}
|
|
|
|
const neighborhoodEntries = [
|
|
buildNeighborhoodEntry({
|
|
path: '/vault/alpha.md',
|
|
title: 'Alpha',
|
|
relatedRefs: ['[[Beta]]'],
|
|
outgoingLinks: ['Beta'],
|
|
modifiedAt: 1700000003,
|
|
}),
|
|
buildNeighborhoodEntry({
|
|
path: '/vault/beta.md',
|
|
title: 'Beta',
|
|
relatedRefs: ['[[Gamma]]'],
|
|
outgoingLinks: ['Gamma'],
|
|
modifiedAt: 1700000002,
|
|
}),
|
|
buildNeighborhoodEntry({
|
|
path: '/vault/gamma.md',
|
|
title: 'Gamma',
|
|
relatedRefs: [],
|
|
outgoingLinks: [],
|
|
modifiedAt: 1700000001,
|
|
}),
|
|
]
|
|
|
|
const neighborhoodContent: Record<string, string> = {
|
|
'/vault/alpha.md': '# Alpha\n\n[[Beta]]',
|
|
'/vault/beta.md': '# Beta\n\n[[Gamma]]',
|
|
'/vault/gamma.md': '# Gamma',
|
|
}
|
|
|
|
function configureNeighborhoodVault() {
|
|
mockCommandResults.list_vault = neighborhoodEntries
|
|
mockCommandResults.get_all_content = neighborhoodContent
|
|
mockCommandResults.get_note_content = ({ path }: { path: string }) => neighborhoodContent[path] ?? ''
|
|
}
|
|
|
|
function configureNeighborhoodFavoritesVault() {
|
|
mockCommandResults.list_vault = neighborhoodEntries.map((entry) =>
|
|
entry.path === '/vault/alpha.md'
|
|
? { ...entry, favorite: true, favoriteIndex: 0 }
|
|
: entry,
|
|
)
|
|
mockCommandResults.get_all_content = neighborhoodContent
|
|
mockCommandResults.get_note_content = ({ path }: { path: string }) => neighborhoodContent[path] ?? ''
|
|
}
|
|
|
|
function getHeaderForNoteList(noteListContainer: HTMLElement) {
|
|
return within(noteListContainer.parentElement as HTMLElement).getByRole('heading', { level: 3 })
|
|
}
|
|
|
|
async function enterNeighborhood(noteListContainer: HTMLElement, title: string) {
|
|
await act(async () => {
|
|
fireEvent.click(within(noteListContainer).getByText(title), { metaKey: true })
|
|
await Promise.resolve()
|
|
})
|
|
}
|
|
|
|
async function pressEscape() {
|
|
await act(async () => {
|
|
fireEvent.keyDown(window, { key: 'Escape' })
|
|
await Promise.resolve()
|
|
})
|
|
}
|
|
|
|
function resetMockCommandResults() {
|
|
Object.assign(mockCommandResults, {
|
|
load_vault_list: mockVaultList,
|
|
list_vault: mockEntries,
|
|
list_vault_folders: [],
|
|
list_views: [],
|
|
get_all_content: mockAllContent,
|
|
get_modified_files: [],
|
|
get_note_content: mockAllContent['/vault/project/test.md'] || '',
|
|
save_note_content: null,
|
|
reload_vault_entry: ({ path }: { path: string }) => mockEntries.find((entry) => entry.path === path) ?? null,
|
|
sync_vault_asset_scope_for_window: null,
|
|
get_file_history: [],
|
|
get_settings: {
|
|
auto_pull_interval_minutes: null,
|
|
auto_advance_inbox_after_organize: null,
|
|
telemetry_consent: true,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
},
|
|
is_git_repo: true,
|
|
init_git_repo: null,
|
|
save_settings: null,
|
|
check_vault_exists: true,
|
|
get_default_vault_path: expectedDefaultVaultPath,
|
|
list_themes: [],
|
|
get_vault_settings: { theme: null },
|
|
})
|
|
}
|
|
|
|
function resolveMockCommandResult(cmd: string, args?: unknown) {
|
|
const result = mockCommandResults[cmd]
|
|
return typeof result === 'function'
|
|
? (result as (input?: unknown) => unknown)(args)
|
|
: result ?? null
|
|
}
|
|
|
|
vi.mock('./mock-tauri', () => ({
|
|
isTauri: vi.fn(() => false),
|
|
mockInvoke: vi.fn(async (cmd: string, args?: unknown) => resolveMockCommandResult(cmd, args)),
|
|
addMockEntry: vi.fn(),
|
|
updateMockContent: vi.fn(),
|
|
trackMockChange: vi.fn(),
|
|
}))
|
|
|
|
// Mock ai-chat utilities
|
|
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 () => {}),
|
|
}))
|
|
|
|
vi.mock('./hooks/useUpdater', async () => {
|
|
const actual = await vi.importActual<typeof import('./hooks/useUpdater')>('./hooks/useUpdater')
|
|
|
|
return {
|
|
...actual,
|
|
useUpdater: vi.fn(() => ({
|
|
status: { state: 'idle' },
|
|
actions: {
|
|
checkForUpdates: vi.fn(async () => ({ kind: 'up-to-date' })),
|
|
startDownload: vi.fn(),
|
|
openReleaseNotes: vi.fn(),
|
|
dismiss: vi.fn(),
|
|
},
|
|
})),
|
|
restartApp: vi.fn(),
|
|
}
|
|
})
|
|
|
|
// Mock BlockNote components (they need DOM APIs not available in jsdom)
|
|
vi.mock('@blocknote/core', () => ({
|
|
BlockNoteSchema: { create: () => ({ extend: () => ({}) }) },
|
|
createCodeBlockSpec: vi.fn(() => ({})),
|
|
createExtension: (factory: unknown) => () => factory,
|
|
defaultInlineContentSpecs: {},
|
|
filterSuggestionItems: vi.fn(() => []),
|
|
}))
|
|
|
|
vi.mock('@blocknote/code-block', () => ({
|
|
codeBlockOptions: {},
|
|
}))
|
|
|
|
vi.mock('@blocknote/core/extensions', () => ({
|
|
filterSuggestionItems: vi.fn(() => []),
|
|
}))
|
|
|
|
vi.mock('@blocknote/react', () => ({
|
|
createReactBlockSpec: () => () => ({}),
|
|
createReactInlineContentSpec: () => ({ render: () => null }),
|
|
BlockNoteViewRaw: ({ children }: { children?: ReactNode }) => (
|
|
<div data-testid="blocknote-view">
|
|
<div contentEditable suppressContentEditableWarning data-testid="mock-editor">
|
|
mock editor
|
|
</div>
|
|
{children}
|
|
</div>
|
|
),
|
|
LinkToolbar: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
|
ComponentsContext: {
|
|
Provider: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
|
},
|
|
useCreateBlockNote: () => ({
|
|
tryParseMarkdownToBlocks: async () => [],
|
|
replaceBlocks: () => {},
|
|
document: [],
|
|
insertInlineContent: () => {},
|
|
setTextCursorPosition: () => {},
|
|
focus: () => {},
|
|
onMount: (cb: () => void) => { cb(); return () => {} },
|
|
}),
|
|
LinkToolbarController: () => null,
|
|
EditLinkButton: () => null,
|
|
DeleteLinkButton: () => null,
|
|
SideMenuController: () => null,
|
|
SuggestionMenuController: () => null,
|
|
useComponentsContext: () => ({
|
|
LinkToolbar: {
|
|
Button: ({
|
|
children,
|
|
label,
|
|
onClick,
|
|
}: { children?: ReactNode; label?: string; onClick?: () => void }) => (
|
|
<button onClick={onClick} type="button">
|
|
{label}
|
|
{children}
|
|
</button>
|
|
),
|
|
},
|
|
}),
|
|
useDictionary: () => ({
|
|
link_toolbar: {
|
|
open: { tooltip: 'Open in a new tab' },
|
|
},
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@blocknote/mantine', () => ({
|
|
components: {},
|
|
BlockNoteView: ({ children }: { children?: React.ReactNode }) => <div data-testid="blocknote-view">{children}</div>,
|
|
}))
|
|
|
|
vi.mock('@blocknote/mantine/style.css', () => ({}))
|
|
|
|
vi.mock('./components/tolariaEditorFormatting', () => ({
|
|
TolariaFormattingToolbar: () => null,
|
|
TolariaFormattingToolbarController: () => null,
|
|
}))
|
|
|
|
import App from './App'
|
|
import { useUpdater } from './hooks/useUpdater'
|
|
import { isTauri } from './mock-tauri'
|
|
import { streamAiAgent } from './utils/streamAiAgent'
|
|
|
|
const AI_AGENTS_ONBOARDING_DISMISSED_KEY = 'tolaria:ai-agents-onboarding-dismissed'
|
|
const CLAUDE_CODE_ONBOARDING_DISMISSED_KEY = 'tolaria:claude-code-onboarding-dismissed'
|
|
|
|
function createMockUpdaterResult(
|
|
checkForUpdates: () => Promise<{ kind: 'up-to-date' } | { kind: 'available'; version: string; displayVersion: string } | { kind: 'error'; message: string }> = async () => ({ kind: 'up-to-date' }),
|
|
) {
|
|
return {
|
|
status: { state: 'idle' as const },
|
|
actions: {
|
|
checkForUpdates,
|
|
startDownload: vi.fn(),
|
|
openReleaseNotes: vi.fn(),
|
|
dismiss: vi.fn(),
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('App', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetMockCommandResults()
|
|
vi.mocked(invoke).mockImplementation(async (cmd: string, args?: unknown) => resolveMockCommandResult(cmd, args))
|
|
vi.mocked(isTauri).mockReturnValue(false)
|
|
vi.mocked(useUpdater).mockReturnValue(createMockUpdaterResult())
|
|
localStorage.clear()
|
|
window.history.replaceState({}, '', '/')
|
|
localStorage.setItem(CLAUDE_CODE_ONBOARDING_DISMISSED_KEY, '1')
|
|
})
|
|
|
|
it('renders the four-panel layout', async () => {
|
|
render(<App />)
|
|
expect(await screen.findByText('All Notes', {}, { timeout: 5000 })).toBeInTheDocument()
|
|
})
|
|
|
|
it('creates custom views with a portable fallback filename for symbol-only names', async () => {
|
|
const savedViews: ViewFile[] = []
|
|
const saveView = vi.fn(({ filename, definition }: { filename: string; definition: ViewDefinition }) => {
|
|
if (filename === '.yml') throw new Error('Invalid view filename')
|
|
savedViews.push({ filename, definition })
|
|
return null
|
|
})
|
|
mockCommandResults.save_view_cmd = saveView
|
|
mockCommandResults.list_views = () => savedViews
|
|
mockCommandResults.reload_vault = mockEntries
|
|
|
|
render(<App />)
|
|
|
|
await screen.findByText('All Notes')
|
|
fireEvent.click(screen.getByRole('button', { name: 'Create view' }))
|
|
const dialog = await screen.findByRole('dialog')
|
|
fireEvent.change(within(dialog).getByPlaceholderText(/Active Projects|Reading List/i), {
|
|
target: { value: '🚀' },
|
|
})
|
|
fireEvent.click(within(dialog).getByRole('button', { name: 'Create' }))
|
|
|
|
await waitFor(() => {
|
|
expect(saveView).toHaveBeenCalledWith(expect.objectContaining({
|
|
filename: 'view.yml',
|
|
definition: expect.objectContaining({ name: '🚀' }),
|
|
}))
|
|
})
|
|
}, 10000)
|
|
|
|
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('keeps the app shell usable while the vault note scan is pending', async () => {
|
|
let resolveListVault: ((value: typeof mockEntries) => void) | null = null
|
|
const listVaultPromise = new Promise<typeof mockEntries>((resolve) => {
|
|
resolveListVault = resolve
|
|
})
|
|
mockCommandResults.list_vault = () => listVaultPromise
|
|
|
|
render(<App />)
|
|
|
|
expect(await screen.findByTestId('sidebar-loading-favorites', {}, { timeout: 5000 })).toBeInTheDocument()
|
|
expect(screen.queryByTestId('vault-loading-skeleton')).not.toBeInTheDocument()
|
|
expect(screen.getByTestId('sidebar-top-nav')).toHaveTextContent('Inbox')
|
|
expect(screen.getByTestId('sidebar-loading-views')).toBeInTheDocument()
|
|
expect(screen.getByTestId('sidebar-loading-types')).toBeInTheDocument()
|
|
expect(screen.getByTestId('sidebar-loading-folders')).toBeInTheDocument()
|
|
expect(screen.getByTestId('note-list-loading-skeleton')).toBeInTheDocument()
|
|
expect(screen.getByTestId('breadcrumb-title-skeleton')).toBeInTheDocument()
|
|
expect(screen.getByTestId('editor-content-skeleton')).toBeInTheDocument()
|
|
expect(screen.queryByText('Select a note to start editing')).not.toBeInTheDocument()
|
|
expect(screen.getByTestId('status-vault-reloading')).toHaveAccessibleName('Reloading vault from disk')
|
|
await act(async () => {
|
|
fireEvent.keyDown(window, { key: 'p', code: 'KeyP', metaKey: true })
|
|
await Promise.resolve()
|
|
})
|
|
expect(within(screen.getByTestId('quick-open-palette')).getByText('Reloading vault...')).toBeInTheDocument()
|
|
|
|
await act(async () => {
|
|
resolveListVault?.(mockEntries)
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('vault-loading-skeleton')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('note-list-loading-skeleton')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('breadcrumb-title-skeleton')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('editor-content-skeleton')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('status-vault-reloading')).not.toBeInTheDocument()
|
|
expect(screen.getAllByText('Test Project').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('opens a note window by loading only the requested entry', async () => {
|
|
const listVault = vi.fn(() => mockEntries)
|
|
const reloadVaultEntry = vi.fn(({ path }: { path: string }) =>
|
|
mockEntries.find((entry) => entry.path === path) ?? null,
|
|
)
|
|
const getNoteContent = vi.fn(({ path }: { path: string }) => mockAllContent[path] ?? '')
|
|
mockCommandResults.list_vault = listVault
|
|
mockCommandResults.reload_vault_entry = reloadVaultEntry
|
|
mockCommandResults.get_note_content = getNoteContent
|
|
window.history.replaceState(
|
|
{},
|
|
'',
|
|
'/?window=note&path=%2Fvault%2Fproject%2Ftest.md&vault=%2Fvault&title=Test+Project',
|
|
)
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => expect(reloadVaultEntry).toHaveBeenCalled())
|
|
expect(reloadVaultEntry).toHaveBeenCalledWith({ path: '/vault/project/test.md', vaultPath: '/vault' })
|
|
await waitFor(() => expect(getNoteContent).toHaveBeenCalled())
|
|
expect(getNoteContent).toHaveBeenCalledWith({ path: '/vault/project/test.md', vaultPath: '/vault' })
|
|
await waitFor(() => expect(window.__laputaTest?.activeTabPath).toBe('/vault/project/test.md'))
|
|
expect(listVault).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows keyboard shortcut hints', async () => {
|
|
const quickOpenHint = formatShortcutDisplay({ display: '⌘P / ⌘O' })
|
|
const newNoteHint = formatShortcutDisplay({ display: '⌘N' })
|
|
const { container } = render(<App />)
|
|
await waitFor(() => {
|
|
const shortcutHint = Array.from(container.querySelectorAll('span.text-xs.text-muted-foreground'))
|
|
.find((element) => element.textContent === `${quickOpenHint} to search · ${newNoteHint} to create`)
|
|
|
|
expect(shortcutHint).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('shows visible feedback when a manual update check finds an update', async () => {
|
|
vi.mocked(useUpdater).mockReturnValue(createMockUpdaterResult(async () => ({
|
|
kind: 'available',
|
|
version: '2026.4.25',
|
|
displayVersion: '2026.4.25',
|
|
})))
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('All Notes')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('status-build-number'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Tolaria 2026.4.25 is available')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows visible feedback when a menu-driven update check finds no eligible update', async () => {
|
|
vi.mocked(useUpdater).mockReturnValue(createMockUpdaterResult(async () => ({ kind: 'up-to-date' })))
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('All Notes')).toBeInTheDocument()
|
|
expect(typeof window.__laputaTest?.dispatchBrowserMenuCommand).toBe('function')
|
|
})
|
|
|
|
act(() => {
|
|
window.__laputaTest?.dispatchBrowserMenuCommand?.('app-check-for-updates')
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('No newer stable update is available right now')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('shows the external AI setup dialog from the menu when AI onboarding is active', async () => {
|
|
localStorage.removeItem(AI_AGENTS_ONBOARDING_DISMISSED_KEY)
|
|
localStorage.removeItem(CLAUDE_CODE_ONBOARDING_DISMISSED_KEY)
|
|
mockCommandResults.get_ai_agents_status = {
|
|
claude_code: { installed: true, version: '2.1.90' },
|
|
codex: { installed: true, version: '0.122.0-alpha.1' },
|
|
opencode: { installed: false, version: null },
|
|
pi: { installed: false, version: null },
|
|
gemini: { installed: false, version: null },
|
|
}
|
|
mockCommandResults.check_mcp_status = 'installed'
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('AI agents ready')).toBeInTheDocument()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(typeof window.__laputaTest?.dispatchBrowserMenuCommand).toBe('function')
|
|
})
|
|
|
|
act(() => {
|
|
window.__laputaTest?.dispatchBrowserMenuCommand?.('vault-install-mcp')
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Manage External AI Tools')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByTestId('mcp-setup-dialog')).toBeInTheDocument()
|
|
expect(screen.queryByText('AI agents ready')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('routes right-panel AI chat messages to the selected default agent', async () => {
|
|
mockCommandResults.get_settings = {
|
|
auto_pull_interval_minutes: null,
|
|
auto_advance_inbox_after_organize: null,
|
|
telemetry_consent: true,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
default_ai_agent: 'codex',
|
|
}
|
|
mockCommandResults.get_ai_agents_status = {
|
|
claude_code: { installed: true, version: '2.1.90' },
|
|
codex: { installed: true, version: '0.122.0-alpha.1' },
|
|
opencode: { installed: false, version: null },
|
|
pi: { installed: false, version: null },
|
|
gemini: { installed: false, version: null },
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
await screen.findByText('All Notes')
|
|
fireEvent.keyDown(window, { key: 'l', code: 'KeyL', metaKey: true, shiftKey: true })
|
|
|
|
const input = await screen.findByTestId('agent-input')
|
|
await waitFor(() => {
|
|
expect(input).toHaveAttribute('aria-placeholder', 'Ask Codex')
|
|
})
|
|
|
|
input.textContent = 'Summarize the active vault'
|
|
fireEvent.input(input)
|
|
fireEvent.click(screen.getByTestId('agent-send'))
|
|
|
|
await waitFor(() => {
|
|
expect(streamAiAgent).toHaveBeenCalledWith(expect.objectContaining({
|
|
agent: 'codex',
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('waits for saved AI agent settings before sending right-panel messages', async () => {
|
|
let resolveSettings: ((settings: Settings) => void) | null = null
|
|
mockCommandResults.get_settings = () => new Promise((resolve) => {
|
|
resolveSettings = resolve
|
|
})
|
|
mockCommandResults.get_ai_agents_status = {
|
|
claude_code: { installed: true, version: '2.1.90' },
|
|
codex: { installed: true, version: '0.122.0-alpha.1' },
|
|
opencode: { installed: false, version: null },
|
|
pi: { installed: false, version: null },
|
|
gemini: { installed: false, version: null },
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
await screen.findByText('All Notes')
|
|
fireEvent.keyDown(window, { key: 'l', code: 'KeyL', metaKey: true, shiftKey: true })
|
|
|
|
const input = await screen.findByTestId('agent-input')
|
|
input.textContent = 'Summarize the active vault'
|
|
fireEvent.input(input)
|
|
fireEvent.click(screen.getByTestId('agent-send'))
|
|
|
|
await act(async () => {
|
|
await Promise.resolve()
|
|
})
|
|
expect(streamAiAgent).not.toHaveBeenCalled()
|
|
|
|
await act(async () => {
|
|
resolveSettings?.({
|
|
auto_pull_interval_minutes: null,
|
|
auto_advance_inbox_after_organize: null,
|
|
telemetry_consent: true,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
default_ai_agent: 'codex',
|
|
})
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(input).toHaveAttribute('aria-placeholder', 'Ask Codex')
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('agent-send'))
|
|
|
|
await waitFor(() => {
|
|
expect(streamAiAgent).toHaveBeenCalledWith(expect.objectContaining({
|
|
agent: 'codex',
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('shows onboarding after telemetry consent when no active vault is configured', async () => {
|
|
mockCommandResults.get_settings = {
|
|
auto_pull_interval_minutes: null,
|
|
telemetry_consent: null,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
}
|
|
mockCommandResults.load_vault_list = { vaults: [], active_vault: null, hidden_defaults: [] }
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Help improve Tolaria')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('telemetry-accept'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('welcome-screen')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Open existing vault')
|
|
})
|
|
|
|
it.each([
|
|
['telemetry-accept', 'Allow anonymous reporting'],
|
|
['telemetry-decline', 'No thanks'],
|
|
])('ignores a remembered default vault after %s when onboarding was never completed', async (buttonTestId) => {
|
|
const rememberedDefaultVaultPath = expectedDefaultVaultPath
|
|
localStorage.setItem('tolaria_welcome_dismissed', '1')
|
|
mockCommandResults.get_default_vault_path = rememberedDefaultVaultPath
|
|
mockCommandResults.get_settings = {
|
|
auto_pull_interval_minutes: null,
|
|
telemetry_consent: null,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
}
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [],
|
|
active_vault: rememberedDefaultVaultPath,
|
|
hidden_defaults: [],
|
|
}
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === rememberedDefaultVaultPath
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Help improve Tolaria')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId(buttonTestId))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('welcome-screen')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Open existing vault')
|
|
})
|
|
|
|
it('keeps startup on a neutral loading state while the last vault is still resolving', async () => {
|
|
localStorage.setItem('tolaria_welcome_dismissed', '1')
|
|
|
|
let resolveVaultList: ((value: typeof mockVaultList) => void) | null = null
|
|
|
|
mockCommandResults.load_vault_list = () =>
|
|
new Promise<typeof mockVaultList>((resolve) => {
|
|
resolveVaultList = resolve
|
|
})
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === '/work'
|
|
|
|
render(<App />)
|
|
|
|
await act(async () => {
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(screen.getByTestId('vault-loading-skeleton')).toBeInTheDocument()
|
|
expect(screen.queryByText('Vault not found')).not.toBeInTheDocument()
|
|
|
|
await act(async () => {
|
|
resolveVaultList?.({
|
|
vaults: [{ label: 'Work Vault', path: '/work' }],
|
|
active_vault: '/work',
|
|
hidden_defaults: [],
|
|
})
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent('Work Vault')
|
|
})
|
|
expect(screen.queryByText('Vault not found')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows the missing-vault screen once the resolved active vault is confirmed missing', async () => {
|
|
localStorage.setItem('tolaria_welcome_dismissed', '1')
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [{ label: 'Old Vault', path: '/missing-vault' }],
|
|
active_vault: '/missing-vault',
|
|
hidden_defaults: [],
|
|
}
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Vault not found')).toBeInTheDocument()
|
|
})
|
|
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Choose a different folder')
|
|
})
|
|
|
|
it('shows welcome instead of vault-missing when the missing path was not a persisted active vault', async () => {
|
|
localStorage.setItem('tolaria_welcome_dismissed', '1')
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [],
|
|
active_vault: null,
|
|
hidden_defaults: [],
|
|
}
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === expectedDefaultVaultPath
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Welcome to Tolaria')).toBeInTheDocument()
|
|
})
|
|
expect(screen.queryByText('Vault not found')).not.toBeInTheDocument()
|
|
expect(screen.getByTestId('welcome-open-folder')).toHaveTextContent('Open existing vault')
|
|
})
|
|
|
|
it('persists and opens an existing vault chosen from onboarding', async () => {
|
|
const selectedVaultPath = '/Users/mock/Documents/Work Vault'
|
|
const selectedVaultUrl = 'file:///Users/mock/Documents/Work%20Vault'
|
|
const saveVaultList = vi.fn()
|
|
const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue(selectedVaultUrl)
|
|
|
|
mockCommandResults.load_vault_list = { vaults: [], active_vault: null, hidden_defaults: [] }
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => args?.path === selectedVaultPath
|
|
mockCommandResults.save_vault_list = (args?: {
|
|
list?: { vaults?: Array<{ label: string; path: string }>; active_vault?: string | null }
|
|
}) => {
|
|
saveVaultList(args)
|
|
return null
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('welcome-screen')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('welcome-open-folder'))
|
|
|
|
await waitFor(() => {
|
|
expect(saveVaultList).toHaveBeenCalledWith({
|
|
list: {
|
|
vaults: [{ label: 'Work Vault', path: selectedVaultPath }],
|
|
active_vault: selectedVaultPath,
|
|
hidden_defaults: [],
|
|
},
|
|
})
|
|
})
|
|
expect(saveVaultList).toHaveBeenCalledTimes(1)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent('Work Vault')
|
|
})
|
|
|
|
promptSpy.mockRestore()
|
|
})
|
|
|
|
it('persists and opens the onboarding template vault after cloning', async () => {
|
|
let templateExists = false
|
|
const saveVaultList = vi.fn()
|
|
const promptSpy = vi.spyOn(window, 'prompt').mockReturnValue('file:///Users/mock/Documents')
|
|
const expectedLabel = 'Getting Started'
|
|
|
|
mockCommandResults.load_vault_list = { vaults: [], active_vault: null, hidden_defaults: [] }
|
|
mockCommandResults.check_vault_exists = (args?: { path?: string }) => {
|
|
if (args?.path === expectedDefaultVaultPath) {
|
|
return templateExists
|
|
}
|
|
return false
|
|
}
|
|
mockCommandResults.create_getting_started_vault = () => {
|
|
templateExists = true
|
|
return expectedDefaultVaultPath
|
|
}
|
|
mockCommandResults.save_vault_list = (args?: {
|
|
list?: { vaults?: Array<{ label: string; path: string }>; active_vault?: string | null }
|
|
}) => {
|
|
saveVaultList(args)
|
|
return null
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('welcome-screen')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByTestId('welcome-create-vault'))
|
|
|
|
await waitFor(() => {
|
|
expect(saveVaultList).toHaveBeenCalledWith({
|
|
list: {
|
|
vaults: [],
|
|
active_vault: expectedDefaultVaultPath,
|
|
hidden_defaults: [],
|
|
},
|
|
})
|
|
})
|
|
expect(saveVaultList).toHaveBeenCalledTimes(1)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent(expectedLabel)
|
|
})
|
|
|
|
promptSpy.mockRestore()
|
|
})
|
|
|
|
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('pressing Escape in Neighborhood mode blurs the editor before unwinding note-list history', async () => {
|
|
configureNeighborhoodVault()
|
|
|
|
render(<App />)
|
|
|
|
const noteListContainer = await screen.findByTestId('note-list-container', {}, { timeout: 5000 })
|
|
const getHeader = () => getHeaderForNoteList(noteListContainer)
|
|
|
|
await waitFor(() => {
|
|
expect(getHeader()).toHaveTextContent('Inbox')
|
|
})
|
|
|
|
await enterNeighborhood(noteListContainer, 'Alpha')
|
|
|
|
await waitFor(() => {
|
|
expect(getHeader()).toHaveTextContent('Alpha')
|
|
})
|
|
|
|
const editor = screen.getByTestId('mock-editor')
|
|
editor.focus()
|
|
expect(editor).toHaveFocus()
|
|
|
|
await pressEscape()
|
|
|
|
await waitFor(() => {
|
|
expect(noteListContainer).toHaveFocus()
|
|
expect(getHeader()).toHaveTextContent('Alpha')
|
|
})
|
|
|
|
await enterNeighborhood(noteListContainer, 'Beta')
|
|
|
|
await waitFor(() => {
|
|
expect(getHeader()).toHaveTextContent('Beta')
|
|
})
|
|
|
|
await pressEscape()
|
|
|
|
await waitFor(() => {
|
|
expect(getHeader()).toHaveTextContent('Alpha')
|
|
})
|
|
|
|
await pressEscape()
|
|
|
|
await waitFor(() => {
|
|
expect(getHeader()).toHaveTextContent('Inbox')
|
|
})
|
|
})
|
|
|
|
it('opens favorites directly into Neighborhood mode', async () => {
|
|
configureNeighborhoodFavoritesVault()
|
|
|
|
render(<App />)
|
|
|
|
const sidebar = await screen.findByText('FAVORITES')
|
|
fireEvent.click(within(sidebar.closest('div')?.parentElement as HTMLElement).getByText('Alpha'))
|
|
|
|
const noteListContainer = await screen.findByTestId('note-list-container')
|
|
await waitFor(() => {
|
|
expect(getHeaderForNoteList(noteListContainer)).toHaveTextContent('Alpha')
|
|
})
|
|
|
|
expect(screen.getByText('Related to')).toBeInTheDocument()
|
|
expect(screen.getByText('Beta')).toBeInTheDocument()
|
|
})
|
|
|
|
it('defaults to All Notes when explicit organization is disabled in vault config', async () => {
|
|
const workVaultPath = '/Users/mock/Documents/Work'
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [{ label: 'Work Vault', path: workVaultPath }],
|
|
active_vault: workVaultPath,
|
|
hidden_defaults: [],
|
|
}
|
|
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:${workVaultPath}`, disabledWorkflowConfig)
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(within(screen.getByTestId('sidebar-top-nav')).queryByText('Inbox')).not.toBeInTheDocument()
|
|
expect(screen.getByText('All Notes')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('auto-advances to the next inbox item after organizing when the setting is enabled', async () => {
|
|
configureNeighborhoodVault()
|
|
mockCommandResults.get_settings = {
|
|
auto_pull_interval_minutes: null,
|
|
auto_advance_inbox_after_organize: true,
|
|
telemetry_consent: true,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
const noteListContainer = await screen.findByTestId('note-list-container')
|
|
await waitFor(() => {
|
|
expect(getHeaderForNoteList(noteListContainer)).toHaveTextContent('Inbox')
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(within(noteListContainer).getByText('Alpha'))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Set note as organized' })).toBeInTheDocument()
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Set note as organized' }))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(window.__laputaTest?.activeTabPath).toBe('/vault/beta.md')
|
|
})
|
|
})
|
|
|
|
it('keeps the manually selected note after organizing finishes later', async () => {
|
|
configureNeighborhoodVault()
|
|
mockCommandResults.get_settings = {
|
|
auto_pull_interval_minutes: null,
|
|
auto_advance_inbox_after_organize: true,
|
|
telemetry_consent: true,
|
|
crash_reporting_enabled: null,
|
|
analytics_enabled: null,
|
|
anonymous_id: null,
|
|
release_channel: null,
|
|
}
|
|
|
|
let resolveOrganizeSave!: () => void
|
|
const organizeSave = new Promise<void>((resolve) => {
|
|
resolveOrganizeSave = resolve
|
|
})
|
|
mockCommandResults.save_note_content = vi.fn(() => organizeSave)
|
|
|
|
render(<App />)
|
|
|
|
const noteListContainer = await screen.findByTestId('note-list-container')
|
|
await waitFor(() => {
|
|
expect(getHeaderForNoteList(noteListContainer)).toHaveTextContent('Inbox')
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(within(noteListContainer).getByText('Alpha'))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: 'Set note as organized' })).toBeInTheDocument()
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Set note as organized' }))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(within(noteListContainer).getByText('Gamma'))
|
|
await Promise.resolve()
|
|
})
|
|
await waitFor(() => {
|
|
expect(window.__laputaTest?.activeTabPath).toBe('/vault/gamma.md')
|
|
})
|
|
|
|
await act(async () => {
|
|
resolveOrganizeSave()
|
|
await organizeSave
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(window.__laputaTest?.activeTabPath).toBe('/vault/gamma.md')
|
|
})
|
|
|
|
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('switches vaults from the bottom bar after onboarding is ready', async () => {
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [
|
|
{ label: 'Test Vault', path: '/work' },
|
|
{ label: 'Work Vault', path: '/vault-2' },
|
|
],
|
|
active_vault: '/work',
|
|
hidden_defaults: [],
|
|
}
|
|
|
|
render(<App />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent('Test Vault')
|
|
})
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Switch vault' }))
|
|
fireEvent.click(screen.getByTestId('vault-menu-item-Work Vault'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent('Work Vault')
|
|
})
|
|
})
|
|
|
|
it('clears the Git setup dialog when switching to a Git-enabled vault', async () => {
|
|
mockCommandResults.load_vault_list = {
|
|
vaults: [
|
|
{ label: 'Missing Git', path: '/work' },
|
|
{ label: 'Git Vault', path: '/vault-2' },
|
|
],
|
|
active_vault: '/work',
|
|
hidden_defaults: [],
|
|
}
|
|
mockCommandResults.is_git_repo = ({ vaultPath }: { vaultPath?: string } = {}) => vaultPath === '/vault-2'
|
|
|
|
render(<App />)
|
|
|
|
expect(await screen.findByText('Enable Git for this vault?')).toBeInTheDocument()
|
|
|
|
fireEvent.click(screen.getByTestId('status-vault-trigger'))
|
|
fireEvent.click(screen.getByTestId('vault-menu-item-Git Vault'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('status-vault-trigger')).toHaveTextContent('Git Vault')
|
|
})
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Enable Git for this vault?')).not.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()
|
|
})
|
|
})
|
|
|
|
it('updates the main-window size constraints when the view mode changes', async () => {
|
|
const { invoke } = await import('@tauri-apps/api/core') as { invoke: ReturnType<typeof vi.fn> }
|
|
|
|
render(<App />)
|
|
await waitFor(() => {
|
|
expect(screen.getByText('All Notes')).toBeInTheDocument()
|
|
})
|
|
|
|
invoke.mockClear()
|
|
|
|
fireEvent.keyDown(window, { key: '1', metaKey: true })
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('update_current_window_min_size', {
|
|
minWidth: 480,
|
|
minHeight: 400,
|
|
growToFit: true,
|
|
})
|
|
})
|
|
|
|
invoke.mockClear()
|
|
|
|
fireEvent.keyDown(window, { key: '3', metaKey: true })
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('update_current_window_min_size', {
|
|
minWidth: 1030,
|
|
minHeight: 400,
|
|
growToFit: true,
|
|
})
|
|
})
|
|
})
|
|
})
|