import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent } from '@testing-library/react' import { DynamicPropertiesPanel, containsWikilinks } from './DynamicPropertiesPanel' import type { VaultEntry } from '../types' const makeEntry = (overrides: Partial = {}): VaultEntry => ({ path: '/vault/note/test.md', filename: 'test.md', title: 'Test Note', isA: 'Note', aliases: [], belongsTo: [], relatedTo: [], status: 'Active', owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', relationships: {}, icon: null, color: null, order: null, ...overrides, }) describe('containsWikilinks', () => { it('returns true for string wikilinks', () => { expect(containsWikilinks('[[My Note]]')).toBe(true) }) it('returns false for non-wikilink strings', () => { expect(containsWikilinks('plain text')).toBe(false) }) it('returns true for arrays containing wikilinks', () => { expect(containsWikilinks(['[[Note1]]', '[[Note2]]'])).toBe(true) }) it('returns false for arrays without wikilinks', () => { expect(containsWikilinks(['tag1', 'tag2'])).toBe(false) }) it('returns false for booleans', () => { expect(containsWikilinks(true)).toBe(false) }) it('returns false for null', () => { expect(containsWikilinks(null)).toBe(false) }) }) describe('DynamicPropertiesPanel', () => { const onUpdateProperty = vi.fn() const onDeleteProperty = vi.fn() const onAddProperty = vi.fn() const onNavigate = vi.fn() beforeEach(() => { vi.clearAllMocks() }) it('renders type row', () => { render( ) expect(screen.getByText('Type')).toBeInTheDocument() expect(screen.getByText('Note')).toBeInTheDocument() }) it('renders word count', () => { render( ) expect(screen.getByText('Words')).toBeInTheDocument() }) it('renders status as colored pill', () => { render( ) // Status rendered with CSS text-transform: uppercase, DOM text is still "Active" expect(screen.getByTitle('Active')).toBeInTheDocument() }) it('renders properties from frontmatter', () => { render( ) expect(screen.getByText('cadence')).toBeInTheDocument() expect(screen.getByText('Weekly')).toBeInTheDocument() expect(screen.getByText('owner')).toBeInTheDocument() expect(screen.getByText('Luca')).toBeInTheDocument() }) it('skips aliases and relationship keys', () => { render( ) // aliases and "Belongs to" should be skipped expect(screen.queryByText('aliases')).not.toBeInTheDocument() expect(screen.queryByText('Belongs to')).not.toBeInTheDocument() expect(screen.getByText('cadence')).toBeInTheDocument() }) it('renders boolean property as toggle', () => { render( ) // Boolean should show as Yes/No toggle const toggleBtn = screen.getByText('\u2717 No') fireEvent.click(toggleBtn) expect(onUpdateProperty).toHaveBeenCalledWith('archived', true) }) it('renders array property as tag pills', () => { render( ) expect(screen.getByText('ai')).toBeInTheDocument() expect(screen.getByText('ml')).toBeInTheDocument() expect(screen.getByText('deep-learning')).toBeInTheDocument() }) it('shows Add property button', () => { render( ) expect(screen.getByText('+ Add property')).toBeInTheDocument() }) it('opens add property form when button clicked', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) expect(screen.getByPlaceholderText('Property name')).toBeInTheDocument() expect(screen.getByPlaceholderText('Value')).toBeInTheDocument() }) it('adds property via the add form', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) const keyInput = screen.getByPlaceholderText('Property name') const valueInput = screen.getByPlaceholderText('Value') fireEvent.change(keyInput, { target: { value: 'priority' } }) fireEvent.change(valueInput, { target: { value: 'high' } }) fireEvent.click(screen.getByText('Add')) expect(onAddProperty).toHaveBeenCalledWith('priority', 'high') }) it('handles navigating to type via click', () => { render( ) fireEvent.click(screen.getByText('Project')) expect(onNavigate).toHaveBeenCalledWith('type/project') }) it('renders modified date', () => { render( ) expect(screen.getByText('Modified')).toBeInTheDocument() }) it('handles editing status', () => { render( ) // Click status pill to start editing (rendered with CSS uppercase, DOM text is "Active") fireEvent.click(screen.getByTitle('Active')) // Should show edit input const input = screen.getByDisplayValue('Active') fireEvent.change(input, { target: { value: 'Done' } }) fireEvent.keyDown(input, { key: 'Enter' }) expect(onUpdateProperty).toHaveBeenCalledWith('Status', 'Done') }) it('deletes property when delete button clicked', () => { render( ) const deleteBtn = screen.getByTitle('Delete property') fireEvent.click(deleteBtn) expect(onDeleteProperty).toHaveBeenCalledWith('custom_field') }) it('coerces true/false strings to booleans on save', () => { render( ) // Edit the value fireEvent.click(screen.getByText('false')) const input = screen.getByDisplayValue('false') fireEvent.change(input, { target: { value: 'true' } }) fireEvent.keyDown(input, { key: 'Enter' }) expect(onUpdateProperty).toHaveBeenCalledWith('archived', true) }) it('coerces numeric strings to numbers on save', () => { render( ) fireEvent.click(screen.getByText('3')) const input = screen.getByDisplayValue('3') fireEvent.change(input, { target: { value: '5' } }) fireEvent.keyDown(input, { key: 'Enter' }) expect(onUpdateProperty).toHaveBeenCalledWith('order', 5) }) it('cancels add form on Escape', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) const keyInput = screen.getByPlaceholderText('Property name') fireEvent.keyDown(keyInput, { key: 'Escape' }) // Form should be hidden, button should reappear expect(screen.getByText('+ Add property')).toBeInTheDocument() }) it('adds property on Enter in form', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) const keyInput = screen.getByPlaceholderText('Property name') const valueInput = screen.getByPlaceholderText('Value') fireEvent.change(keyInput, { target: { value: 'key' } }) fireEvent.change(valueInput, { target: { value: 'val' } }) fireEvent.keyDown(valueInput, { key: 'Enter' }) expect(onAddProperty).toHaveBeenCalledWith('key', 'val') }) it('handles comma-separated values as array', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) const keyInput = screen.getByPlaceholderText('Property name') const valueInput = screen.getByPlaceholderText('Value') fireEvent.change(keyInput, { target: { value: 'tags' } }) fireEvent.change(valueInput, { target: { value: 'a, b, c' } }) fireEvent.keyDown(valueInput, { key: 'Enter' }) expect(onAddProperty).toHaveBeenCalledWith('tags', ['a', 'b', 'c']) }) it('handles cancel button in add form', () => { render( ) fireEvent.click(screen.getByText('+ Add property')) fireEvent.click(screen.getByText('Cancel')) expect(screen.getByText('+ Add property')).toBeInTheDocument() }) })