Files
tolaria/src/NoteWindowApp.test.tsx
2026-05-21 12:01:14 +02:00

149 lines
3.9 KiB
TypeScript

import { act, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import NoteWindowApp from './NoteWindowApp'
import type { NoteStatus, VaultEntry } from './types'
type MockEditorProps = {
activeTabPath: string | null
entries: VaultEntry[]
getNoteStatus?: (path: string) => NoteStatus
onContentChange?: (path: string, content: string) => void
tabs: Array<{ entry: VaultEntry; content: string }>
vaultPath?: string
vaultPaths?: string[]
}
const mocks = vi.hoisted(() => ({
editorProps: [] as MockEditorProps[],
invoke: vi.fn(),
setTitle: vi.fn(),
warn: vi.fn(),
}))
vi.mock('@tauri-apps/api/core', () => ({
invoke: mocks.invoke,
}))
vi.mock('@tauri-apps/api/window', () => ({
getCurrentWindow: () => ({ setTitle: mocks.setTitle }),
}))
vi.mock('./mock-tauri', () => ({
isTauri: () => true,
mockInvoke: vi.fn(),
}))
vi.mock('./components/Editor', () => ({
Editor: (props: MockEditorProps) => {
mocks.editorProps.push(props)
return (
<button
data-testid="mock-note-window-editor"
type="button"
onClick={() => props.onContentChange?.(props.activeTabPath ?? '', 'Updated content')}
>
{props.tabs[0]?.content}
</button>
)
},
}))
vi.mock('./components/Toast', () => ({
Toast: ({ message }: { message: string | null }) => (
<div data-testid="mock-toast">{message}</div>
),
}))
function makeEntry(): VaultEntry {
return {
path: 'Notes/entry.md',
filename: 'entry.md',
title: 'Entry',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: 1_700_000_000,
createdAt: null,
fileSize: 256,
snippet: '',
wordCount: 10,
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',
}
}
function setSearch(search: string) {
Object.defineProperty(window, 'location', {
writable: true,
value: { ...window.location, search },
})
}
describe('NoteWindowApp', () => {
beforeEach(() => {
vi.useRealTimers()
vi.clearAllMocks()
vi.spyOn(console, 'warn').mockImplementation(mocks.warn)
mocks.editorProps.length = 0
setSearch('?window=note&path=%2Fvault%2FNotes%2Fentry.md&vault=%2Fvault&title=Entry')
mocks.invoke.mockImplementation(async (command: string) => {
if (command === 'sync_vault_asset_scope_for_window') return undefined
if (command === 'reload_vault_entry') return makeEntry()
if (command === 'get_note_content') return '# Entry'
if (command === 'save_note_content') return undefined
throw new Error(`Unexpected command: ${command}`)
})
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
it('loads one note without booting the full vault and saves editor changes', async () => {
render(<NoteWindowApp />)
const editor = await screen.findByTestId('mock-note-window-editor')
expect(editor).toHaveTextContent('# Entry')
expect(mocks.editorProps.at(-1)).toEqual(expect.objectContaining({
activeTabPath: 'Notes/entry.md',
vaultPath: '/vault',
vaultPaths: ['/vault'],
}))
expect(mocks.invoke).not.toHaveBeenCalledWith('list_vault', expect.anything())
vi.useFakeTimers()
fireEvent.click(editor)
expect(mocks.editorProps.at(-1)?.getNoteStatus?.('Notes/entry.md')).toBe('unsaved')
await act(async () => {
await vi.advanceTimersByTimeAsync(700)
})
vi.useRealTimers()
expect(mocks.invoke).toHaveBeenCalledWith('save_note_content', {
path: 'Notes/entry.md',
content: 'Updated content',
vaultPath: '/vault',
})
})
})