import type { ComponentProps } from 'react' import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest' import { render, screen, fireEvent, waitFor, within } from '@testing-library/react' import { DynamicPropertiesPanel, containsWikilinks } from './DynamicPropertiesPanel' import type { VaultEntry } from '../types' import { bindVaultConfigStore, getVaultConfig, resetVaultConfigStore } from '../utils/vaultConfigStore' import { initDisplayModeOverrides } from '../utils/propertyTypes' import { TooltipProvider } from '@/components/ui/tooltip' // Radix Select needs ResizeObserver and pointer/scroll APIs in JSDOM beforeAll(() => { global.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } Element.prototype.scrollIntoView = vi.fn() Element.prototype.hasPointerCapture = () => false Element.prototype.setPointerCapture = vi.fn() Element.prototype.releasePointerCapture = vi.fn() // Radix Popover needs getComputedStyle in JSDOM if (!window.getComputedStyle) window.getComputedStyle = vi.fn().mockReturnValue({}) as never }) 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, modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100, snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [], ...overrides, }) type DynamicPropertiesPanelProps = ComponentProps type RenderPanelOptions = Omit & { entry?: VaultEntry content?: string frontmatter?: Record } const renderPanel = ({ entry = makeEntry(), content = '', frontmatter = {}, ...props }: RenderPanelOptions = {}) => render( , ) 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() }) function renderEditablePanel(frontmatter: Record) { renderPanel({ frontmatter, onUpdateProperty }) } function openAddPropertyForm(options: RenderPanelOptions = {}) { renderPanel({ onAddProperty, ...options }) fireEvent.click(screen.getByText('Add property')) return { keyInput: screen.getByPlaceholderText('Property name'), valueInput: screen.getByPlaceholderText('Value'), } } it('renders type row', () => { renderPanel({ content: '# Test\n\nSome words here', frontmatter: { Status: 'Active' }, onUpdateProperty, }) expect(screen.getByText('Type')).toBeInTheDocument() expect(screen.getByText('Note')).toBeInTheDocument() }) it('shows the shared type icon in the type row label', () => { renderPanel({ content: '# Test\n\nSome words here', frontmatter: { Status: 'Active' }, onUpdateProperty, }) expect(screen.getByTestId('type-row-icon')).toBeInTheDocument() }) it('renders status as colored pill', () => { renderPanel({ frontmatter: { Status: 'Active' } }) // Status rendered as sentence case expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) it('renders properties from frontmatter', () => { renderPanel({ frontmatter: { cadence: 'Weekly', owner: 'Luca' } }) expect(screen.getByText('Cadence')).toBeInTheDocument() expect(screen.getByText('Weekly')).toBeInTheDocument() expect(screen.getByText('Owner')).toBeInTheDocument() expect(screen.getByText('Luca')).toBeInTheDocument() }) it('renders capitalized Owner with plain text value in Properties panel', () => { renderPanel({ frontmatter: { Owner: 'Luca' } }) expect(screen.getByText('Owner')).toBeInTheDocument() expect(screen.getByText('Luca')).toBeInTheDocument() }) it('left-aligns mixed property value displays', () => { renderPanel({ frontmatter: { Owner: 'Luca', History_confidence: 0.84, Date: '2026-04-11', color: '#3b82f6', Window_end: null, }, }) expect(screen.getByText('Luca').parentElement).toHaveClass('justify-start', 'text-left') expect(screen.getByText('0.84').parentElement).toHaveClass('justify-start', 'text-left') expect(screen.getByTestId('date-display')).toHaveClass('text-left') expect(screen.getByText('#3b82f6')).toHaveClass('text-left') expect(screen.getByText('\u2014').parentElement).toHaveClass('justify-start', 'text-left') }) it('keeps present empty properties visible and editable', () => { renderPanel({ frontmatter: { 'start date': '' }, onUpdateProperty }) expect(screen.getByText('Start date')).toBeInTheDocument() fireEvent.click(screen.getByText('\u2014')) const input = screen.getByDisplayValue('') fireEvent.change(input, { target: { value: '2026-05-03' } }) fireEvent.blur(input) expect(onUpdateProperty).toHaveBeenCalledWith('start date', '2026-05-03') }) it('shows missing type-defined properties as gray editable placeholders', () => { const typeEntry = makeEntry({ title: 'Book', isA: 'Type', properties: { 'start date': null, }, }) renderPanel({ entry: makeEntry({ title: 'Dune', isA: 'Book' }), entries: [typeEntry], frontmatter: {}, onUpdateProperty, }) const placeholder = screen.getByTestId('type-derived-property') expect(within(placeholder).getByText('Start date')).toHaveClass('text-muted-foreground/40') fireEvent.click(placeholder) const input = screen.getByDisplayValue('') fireEvent.change(input, { target: { value: '2026-05-04' } }) fireEvent.blur(input) expect(onUpdateProperty).toHaveBeenCalledWith('start date', '2026-05-04') }) it('hides Owner with wikilink value from Properties panel', () => { renderPanel({ frontmatter: { Owner: '[[person/luca]]' } }) // Owner with wikilink goes to RelationshipsPanel, not Properties expect(screen.queryByText('Owner')).not.toBeInTheDocument() }) it('renders notion_id as a visible property', () => { renderPanel({ frontmatter: { notion_id: 'abc-123-def' } }) expect(screen.getByText('Notion id')).toBeInTheDocument() expect(screen.getByText('abc-123-def')).toBeInTheDocument() }) it('skips aliases and fields with wikilink values', () => { renderPanel({ frontmatter: { aliases: ['AL'], 'Belongs to': '[[Something]]', cadence: 'Monthly' }, }) // aliases skipped (in SKIP_KEYS); 'Belongs to' skipped (has wikilinks) expect(screen.queryByText('aliases')).not.toBeInTheDocument() expect(screen.queryByText('Belongs to')).not.toBeInTheDocument() expect(screen.getByText('Cadence')).toBeInTheDocument() }) it('shows former relationship key with plain text value in Properties', () => { renderPanel({ frontmatter: { 'Belongs to': 'some-team', cadence: 'Monthly' } }) // 'Belongs to' has a plain text value, not a wikilink — should render as property expect(screen.getByText('Belongs to')).toBeInTheDocument() expect(screen.getByText('some-team')).toBeInTheDocument() }) it('localizes UI actions without translating stored property names', () => { renderPanel({ frontmatter: { Status: 'Active', 'Belongs to': 'some-team' }, onAddProperty, locale: 'zh-CN', }) expect(screen.getByText('Type')).toBeInTheDocument() expect(screen.getByText('Status')).toBeInTheDocument() expect(screen.getByText('Belongs to')).toBeInTheDocument() expect(screen.getByRole('button', { name: '添加属性' })).toBeInTheDocument() }) it('hides custom field with wikilink value from Properties', () => { renderPanel({ frontmatter: { Mentor: '[[person/luca]]' } }) // Mentor contains a wikilink → shown in Relationships, not Properties expect(screen.queryByText('Mentor')).not.toBeInTheDocument() }) it('skips is_a, Is A, and type keys (shown via TypeRow instead)', () => { renderPanel({ frontmatter: { is_a: 'Note', type: 'Note', 'Is A': 'Note', Status: 'Active' }, }) expect(screen.queryByText('is_a')).not.toBeInTheDocument() expect(screen.queryByText('Is A')).not.toBeInTheDocument() // 'type' as a property label should not appear (the TypeRow renders 'Type' differently) const typeLabels = screen.getAllByText('Type') // Only the TypeRow label should exist, not a property row expect(typeLabels).toHaveLength(1) expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) it('renders boolean property as toggle', () => { renderEditablePanel({ published: false }) // Boolean should show as Yes/No toggle const toggleBtn = screen.getByText('No') fireEvent.click(toggleBtn) expect(onUpdateProperty).toHaveBeenCalledWith('published', true) }) it('renders array property as tag pills', () => { renderEditablePanel({ tags: ['ai', 'ml', 'deep-learning'] }) expect(screen.getByText('ai')).toBeInTheDocument() expect(screen.getByText('ml')).toBeInTheDocument() expect(screen.getByText('deep-learning')).toBeInTheDocument() }) it('shows Add property button', () => { renderPanel({ onAddProperty }) expect(screen.getByText('Add property')).toBeInTheDocument() }) it('opens add property form when button clicked', () => { const { keyInput, valueInput } = openAddPropertyForm() expect(keyInput).toBeInTheDocument() expect(valueInput).toBeInTheDocument() expect(screen.getByTestId('add-property-type-trigger')).toBeInTheDocument() }) it('adds property via the add form', () => { const { keyInput, valueInput } = openAddPropertyForm() fireEvent.change(keyInput, { target: { value: 'priority' } }) fireEvent.change(valueInput, { target: { value: 'high' } }) fireEvent.click(screen.getByTestId('add-property-confirm')) expect(onAddProperty).toHaveBeenCalledWith('priority', 'high') }) it('handles navigating to type via click in read-only mode', () => { render( , ) fireEvent.click(screen.getByText('Project')) expect(onNavigate).toHaveBeenCalledWith('project') }) describe('TypeSelector', () => { const typeEntries = [ makeEntry({ path: '/vault/project.md', title: 'Project', isA: 'Type' }), makeEntry({ path: '/vault/person.md', title: 'Person', isA: 'Type' }), makeEntry({ path: '/vault/topic.md', title: 'Topic', isA: 'Type' }), ] it('renders as dropdown when editable', () => { renderPanel({ entries: typeEntries, onUpdateProperty }) expect(screen.getByTestId('type-selector')).toBeInTheDocument() expect(screen.getByRole('combobox')).toBeInTheDocument() }) it('shows available types in dropdown', () => { renderPanel({ entries: typeEntries, onUpdateProperty }) fireEvent.pointerDown(screen.getByRole('combobox'), { button: 0, pointerType: 'mouse' }) expect(screen.getByRole('option', { name: 'None' })).toBeInTheDocument() expect(screen.getByRole('option', { name: 'Person' })).toBeInTheDocument() expect(screen.getByRole('option', { name: 'Project' })).toBeInTheDocument() expect(screen.getByRole('option', { name: 'Topic' })).toBeInTheDocument() }) function openAndSelect(optionName: string) { fireEvent.pointerDown(screen.getByRole('combobox'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: optionName })) } it('calls onUpdateProperty when type selected', () => { renderPanel({ entries: typeEntries, onUpdateProperty }) openAndSelect('Project') expect(onUpdateProperty).toHaveBeenCalledWith('type', 'Project') }) it('clears type when None selected', () => { renderPanel({ entry: makeEntry({ isA: 'Project' }), entries: typeEntries, onUpdateProperty, }) openAndSelect('None') expect(onUpdateProperty).toHaveBeenCalledWith('type', null) }) it('shows current type even when not in available types', () => { renderPanel({ entry: makeEntry({ isA: 'CustomType' }), entries: typeEntries, onUpdateProperty, }) fireEvent.pointerDown(screen.getByRole('combobox'), { button: 0, pointerType: 'mouse' }) expect(screen.getByRole('option', { name: 'CustomType' })).toBeInTheDocument() }) it('shows a missing-type warning with keyboard-accessible help and creation flow', async () => { const onCreateMissingType = vi.fn().mockResolvedValue(undefined) renderPanel({ entry: makeEntry({ isA: 'Hotel' }), entries: typeEntries, onUpdateProperty, onCreateMissingType, }) const warningButton = screen.getByTestId('missing-type-warning') expect(warningButton).toBeInTheDocument() fireEvent.focus(warningButton) await waitFor(() => { expect(screen.getByRole('tooltip')).toHaveTextContent('Missing type') }) fireEvent.click(warningButton) expect(screen.getByDisplayValue('Hotel')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Create' })) await waitFor(() => expect(onCreateMissingType).toHaveBeenCalledWith('Hotel')) }) it('does not show a missing-type warning when the type already exists', () => { renderPanel({ entry: makeEntry({ isA: 'Project' }), entries: typeEntries, onUpdateProperty, }) expect(screen.queryByTestId('missing-type-warning')).not.toBeInTheDocument() }) it('shows None placeholder when entry has no type', () => { renderPanel({ entry: makeEntry({ isA: null }), entries: typeEntries, onUpdateProperty, }) expect(screen.getByTestId('type-selector')).toBeInTheDocument() expect(screen.getByText('None')).toBeInTheDocument() }) }) it('opens status dropdown on click and selects a status', () => { renderEditablePanel({ Status: 'Active' }) // Click status pill to open dropdown fireEvent.click(screen.getByTestId('status-badge')) // Should show dropdown with search input expect(screen.getByTestId('status-dropdown')).toBeInTheDocument() expect(screen.getByTestId('status-search-input')).toBeInTheDocument() // Click on "Done" option in the suggested list fireEvent.click(screen.getByTestId('status-option-Done')) expect(onUpdateProperty).toHaveBeenCalledWith('Status', 'Done') }) it('deletes property when delete button clicked', () => { renderPanel({ frontmatter: { custom_field: 'value' }, onDeleteProperty, onUpdateProperty, }) const deleteBtn = screen.getByTitle('Delete property') fireEvent.click(deleteBtn) expect(onDeleteProperty).toHaveBeenCalledWith('custom_field') }) it('coerces true/false strings to booleans on save', () => { renderEditablePanel({ draft: 'false' }) // 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('draft', true) }) it('coerces numeric strings to numbers on save', () => { renderEditablePanel({ priority: '3' }) fireEvent.click(screen.getByText('3')) const input = screen.getByDisplayValue('3') fireEvent.change(input, { target: { value: '5' } }) fireEvent.keyDown(input, { key: 'Enter' }) expect(onUpdateProperty).toHaveBeenCalledWith('priority', 5) }) it('cancels add form on Escape', () => { const { keyInput } = openAddPropertyForm() 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', () => { const { keyInput, valueInput } = openAddPropertyForm() 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', () => { const { keyInput, valueInput } = openAddPropertyForm() 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', () => { openAddPropertyForm() fireEvent.click(screen.getByTestId('add-property-cancel')) expect(screen.getByText('Add property')).toBeInTheDocument() }) describe('editable vs read-only distinction', () => { it('editable properties have hover styling via data-testid', () => { renderPanel({ frontmatter: { cadence: 'Weekly', owner: 'Luca' }, onUpdateProperty, onDeleteProperty, }) const editableRows = screen.getAllByTestId('editable-property') expect(editableRows.length).toBe(2) // Editable rows have hover:bg-muted class for interactivity editableRows.forEach(row => { expect(row.className).toContain('hover:bg-muted') }) }) }) describe('property row 50/50 layout', () => { it('uses CSS grid with two equal columns on editable rows', () => { renderEditablePanel({ url: 'https://example.com/very/long/path/that/should/be/truncated' }) const editableRows = screen.getAllByTestId('editable-property') editableRows.forEach(row => { expect(row.className).toContain('grid') expect(row.className).toContain('grid-cols-2') }) }) }) describe('suggested property slots', () => { function findSuggestedSlot(label: string): HTMLElement { const slot = screen.getAllByTestId('suggested-property').find((node) => node.textContent?.includes(label)) expect(slot).toBeDefined() return slot as HTMLElement } it('shows Status/Date/URL/Icon slots when no properties exist and onAddProperty provided', () => { renderPanel({ onAddProperty }) const slots = screen.getAllByTestId('suggested-property') expect(slots.length).toBe(4) expect(screen.getByText('Status')).toBeInTheDocument() expect(screen.getByText('Date')).toBeInTheDocument() expect(screen.getByText('URL')).toBeInTheDocument() expect(screen.getByText('Icon')).toBeInTheDocument() }) it('shows a property-kind icon for each suggested slot', () => { renderPanel({ onAddProperty }) expect(within(findSuggestedSlot('Status')).getByTestId('suggested-property-icon-status')).toBeInTheDocument() expect(within(findSuggestedSlot('Date')).getByTestId('suggested-property-icon-date')).toBeInTheDocument() expect(within(findSuggestedSlot('URL')).getByTestId('suggested-property-icon-url')).toBeInTheDocument() expect(within(findSuggestedSlot('Icon')).getByTestId('suggested-property-icon-text')).toBeInTheDocument() }) it('hides Status slot when Status property already exists', () => { renderPanel({ frontmatter: { Status: 'Active' }, onAddProperty, onUpdateProperty, }) const slots = screen.getAllByTestId('suggested-property') expect(slots.length).toBe(3) expect(screen.queryAllByText('Status').some(el => el.closest('[data-testid="suggested-property"]'))).toBe(false) }) it('hides all slots when all suggested properties exist', () => { renderPanel({ frontmatter: { Status: 'Active', Date: '2024-01-01', URL: 'https://example.com', icon: 'star' }, onAddProperty, onUpdateProperty, }) expect(screen.queryByTestId('suggested-property')).not.toBeInTheDocument() }) it('does not show slots when onAddProperty is not provided', () => { renderPanel() expect(screen.queryByTestId('suggested-property')).not.toBeInTheDocument() }) it('opens the status editor without writing an empty property when clicking a suggested slot', async () => { renderPanel({ onAddProperty }) fireEvent.click(findSuggestedSlot('Status')) expect(onAddProperty).not.toHaveBeenCalled() await waitFor(() => { expect(document.querySelector('[data-testid="status-dropdown-popover"]')).toBeInTheDocument() }) }) it('writes the suggested status only after the user picks a value', async () => { renderPanel({ onAddProperty, onUpdateProperty }) fireEvent.click(findSuggestedSlot('Status')) await waitFor(() => { expect(document.querySelector('[data-testid="status-option-Active"]')).toBeInTheDocument() }) fireEvent.click(document.querySelector('[data-testid="status-option-Active"]') as Element) expect(onAddProperty).toHaveBeenCalledWith('Status', 'Active') }) it('cancels a suggested status edit without writing frontmatter', async () => { renderPanel({ onAddProperty }) fireEvent.click(findSuggestedSlot('Status')) await waitFor(() => { expect(document.querySelector('[data-testid="status-search-input"]')).toBeInTheDocument() }) fireEvent.keyDown(document.querySelector('[data-testid="status-search-input"]') as Element, { key: 'Escape' }) expect(onAddProperty).not.toHaveBeenCalled() expect(findSuggestedSlot('Status')).toBeInTheDocument() }) it('opens the date picker without writing an empty property when clicking the Date slot', async () => { renderPanel({ onAddProperty }) fireEvent.click(findSuggestedSlot('Date')) expect(onAddProperty).not.toHaveBeenCalled() await waitFor(() => { expect(screen.getByTestId('date-picker-popover')).toBeInTheDocument() }) }) }) describe('URL property rendering', () => { it('renders URL values with link styling instead of plain EditableValue', () => { renderEditablePanel({ url: 'https://example.com' }) expect(screen.getByTestId('url-link')).toBeInTheDocument() expect(screen.getByTestId('url-link')).toHaveTextContent('https://example.com') }) it('gives long URL values a truncating value-cell layout inside the properties panel', () => { renderEditablePanel({ url: 'https://example.com/very/long/path/that/should/truncate' }) const link = screen.getByTestId('url-link') const value = screen.getByText('https://example.com/very/long/path/that/should/truncate') expect(link).toHaveClass('flex-1', 'overflow-hidden') expect(value).toHaveClass('truncate') }) it('renders bare domain values as URL links', () => { renderEditablePanel({ website: 'example.com' }) expect(screen.getByTestId('url-link')).toBeInTheDocument() }) it('does not render plain text as URL link', () => { renderEditablePanel({ cadence: 'Weekly' }) expect(screen.queryByTestId('url-link')).not.toBeInTheDocument() }) it('shows edit button on URL property', () => { renderEditablePanel({ url: 'https://example.com' }) expect(screen.getByTestId('url-edit-btn')).toBeInTheDocument() }) it('enters edit mode when edit button clicked on URL property', () => { renderEditablePanel({ url: 'https://example.com' }) fireEvent.click(screen.getByTestId('url-edit-btn')) expect(screen.getByDisplayValue('https://example.com')).toBeInTheDocument() }) }) describe('smart property display — date', () => { it('renders date property with friendly format', () => { renderEditablePanel({ deadline: '2026-03-31' }) expect(screen.getByTestId('date-display')).toBeInTheDocument() expect(screen.getByText('Mar 31, 2026')).toBeInTheDocument() }) it('renders date trigger button', () => { renderEditablePanel({ deadline: '2026-03-31' }) const trigger = screen.getByTestId('date-display') expect(trigger.tagName).toBe('BUTTON') }) it('opens calendar popover when date button clicked', () => { renderEditablePanel({ deadline: '2026-03-31' }) fireEvent.click(screen.getByTestId('date-display')) expect(screen.getByTestId('date-picker-popover')).toBeInTheDocument() // Clear button is inside the popover portal expect(screen.getByText('Clear date')).toBeInTheDocument() }) }) describe('smart property display — number', () => { it('renders numeric properties with the number display affordance', () => { renderEditablePanel({ estimate: -3.25 }) expect(screen.getByTestId('number-display')).toBeInTheDocument() expect(screen.getByText('-3.25')).toBeInTheDocument() }) }) describe('smart property display — status auto-detection', () => { it('renders status badge for property named Status', () => { renderEditablePanel({ Status: 'Active' }) expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) it('renders status badge for known status values', () => { renderEditablePanel({ phase: 'Draft' }) expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) }) describe('status dropdown interaction', () => { it('closes dropdown on Escape without saving', () => { renderEditablePanel({ Status: 'Active' }) fireEvent.click(screen.getByTestId('status-badge')) expect(screen.getByTestId('status-dropdown')).toBeInTheDocument() fireEvent.keyDown(screen.getByTestId('status-search-input'), { key: 'Escape' }) expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument() expect(onUpdateProperty).not.toHaveBeenCalled() }) it('closes dropdown on backdrop click without saving', () => { renderEditablePanel({ Status: 'Active' }) fireEvent.click(screen.getByTestId('status-badge')) fireEvent.click(screen.getByTestId('status-dropdown-backdrop')) expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument() expect(onUpdateProperty).not.toHaveBeenCalled() }) it('creates custom status by typing and pressing Enter', () => { renderEditablePanel({ Status: 'Active' }) fireEvent.click(screen.getByTestId('status-badge')) const input = screen.getByTestId('status-search-input') fireEvent.change(input, { target: { value: 'Needs Review' } }) fireEvent.keyDown(input, { key: 'Enter' }) expect(onUpdateProperty).toHaveBeenCalledWith('Status', 'Needs Review') }) it('shows vault statuses from entries', () => { const entriesWithStatuses = [ makeEntry({ path: '/vault/a.md', status: 'Reviewing' }), makeEntry({ path: '/vault/b.md', status: 'Shipped' }), ] renderPanel({ frontmatter: { Status: 'Active' }, entries: entriesWithStatuses, onUpdateProperty, }) fireEvent.click(screen.getByTestId('status-badge')) expect(screen.getByTestId('status-option-Reviewing')).toBeInTheDocument() expect(screen.getByTestId('status-option-Shipped')).toBeInTheDocument() }) }) describe('smart property display — boolean', () => { it('renders boolean toggle for true values', () => { renderEditablePanel({ published: true }) expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() expect(screen.getByText('Yes')).toBeInTheDocument() }) it('renders boolean toggle for false values', () => { renderEditablePanel({ published: false }) expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() expect(screen.getByText('No')).toBeInTheDocument() }) }) describe('system property filtering', () => { it('hides archived metadata but keeps the note icon visible in the properties panel', () => { renderEditablePanel({ archived: false, archived_at: '', icon: '📝', cadence: 'Weekly' }) expect(screen.queryByText('Archived')).not.toBeInTheDocument() expect(screen.queryByText('Archived at')).not.toBeInTheDocument() expect(screen.getByText('Icon')).toBeInTheDocument() expect(screen.getByText('📝')).toBeInTheDocument() expect(screen.getByText('Cadence')).toBeInTheDocument() }) it('keeps the note icon visible even when cased differently', () => { renderEditablePanel({ Archived: false, Icon: '🎯', cadence: 'Daily' }) expect(screen.queryByText('Archived')).not.toBeInTheDocument() expect(screen.getByText('Icon')).toBeInTheDocument() expect(screen.getByText('🎯')).toBeInTheDocument() expect(screen.getByText('Cadence')).toBeInTheDocument() }) it('does not filter similar but non-matching property names', () => { renderEditablePanel({ 'Is Trashed': true, archive_date: '2026-01-01' }) expect(screen.getByText('Is Trashed')).toBeInTheDocument() expect(screen.getByText('Archive date')).toBeInTheDocument() }) }) describe('display mode override', () => { beforeEach(() => { resetVaultConfigStore() bindVaultConfigStore( { zoom: null, view_mode: null, editor_mode: null, tag_colors: null, status_colors: null, property_display_modes: null }, vi.fn(), ) initDisplayModeOverrides({}) }) it('renders display mode trigger on property rows', () => { renderEditablePanel({ cadence: 'Weekly' }) expect(screen.getByTestId('display-mode-trigger')).toBeInTheDocument() }) it('opens display mode menu on trigger click', () => { renderEditablePanel({ cadence: 'Weekly' }) fireEvent.click(screen.getByTestId('display-mode-trigger')) expect(screen.getByTestId('display-mode-menu')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-text')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-number')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-date')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-boolean')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-status')).toBeInTheDocument() expect(screen.getByTestId('display-mode-option-url')).toBeInTheDocument() }) it('persists override to vault config when mode selected', () => { renderEditablePanel({ cadence: 'Weekly' }) fireEvent.click(screen.getByTestId('display-mode-trigger')) fireEvent.click(screen.getByTestId('display-mode-option-status')) const stored = getVaultConfig().property_display_modes as Record expect(stored).toBeTruthy() expect(stored.cadence).toBe('status') }) it('overrides rendering to status badge when status mode selected', () => { initDisplayModeOverrides({ cadence: 'status' }) renderEditablePanel({ cadence: 'Weekly' }) expect(screen.getByTestId('status-badge')).toBeInTheDocument() }) it('renders boolean toggle for string "true" when boolean mode overridden', () => { initDisplayModeOverrides({ draft: 'boolean' }) renderEditablePanel({ draft: 'true' }) expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() expect(screen.getByText('Yes')).toBeInTheDocument() }) it('renders boolean toggle for string "false" when boolean mode overridden', () => { initDisplayModeOverrides({ draft: 'boolean' }) renderEditablePanel({ draft: 'false' }) expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument() expect(screen.getByText('No')).toBeInTheDocument() }) it('toggles string boolean from false to true', () => { initDisplayModeOverrides({ draft: 'boolean' }) renderEditablePanel({ draft: 'false' }) fireEvent.click(screen.getByTestId('boolean-toggle')) expect(onUpdateProperty).toHaveBeenCalledWith('draft', true) }) it('renders date picker for empty value when date mode overridden', () => { initDisplayModeOverrides({ due: 'date' }) renderEditablePanel({ due: '' }) expect(screen.getByTestId('date-display')).toBeInTheDocument() expect(screen.getByText('Pick a date\u2026')).toBeInTheDocument() }) it('renders date picker for non-date string when date mode overridden', () => { initDisplayModeOverrides({ deadline: 'date' }) renderEditablePanel({ deadline: 'soon' }) expect(screen.getByTestId('date-display')).toBeInTheDocument() }) }) describe('type-aware add property form', () => { it('shows number input when number type selected', () => { openAddPropertyForm() fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Number/ })) expect(screen.getByTestId('add-property-number-input')).toBeInTheDocument() }) it('shows boolean toggle when boolean type selected', () => { openAddPropertyForm() // Switch type to boolean fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) expect(screen.getByTestId('add-property-boolean-toggle')).toBeInTheDocument() expect(screen.getByText('\u2717 No')).toBeInTheDocument() }) it('toggles boolean value in add form', () => { openAddPropertyForm() fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) // Toggle from No to Yes fireEvent.click(screen.getByTestId('add-property-boolean-toggle')) expect(screen.getByText('\u2713 Yes')).toBeInTheDocument() }) it('stores actual boolean value when adding boolean property', { timeout: 15_000 }, () => { const { keyInput } = openAddPropertyForm() fireEvent.change(keyInput, { target: { value: 'published' } }) // Switch to boolean type fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Boolean/ })) // Default is false, toggle to true fireEvent.click(screen.getByTestId('add-property-boolean-toggle')) // Submit fireEvent.click(screen.getByTestId('add-property-confirm')) expect(onAddProperty).toHaveBeenCalledWith('published', true) }) it('stores trimmed decimal values as numbers when adding number properties', () => { const { keyInput } = openAddPropertyForm() fireEvent.change(keyInput, { target: { value: 'estimate' } }) fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Number/ })) fireEvent.change(screen.getByTestId('add-property-number-input'), { target: { value: ' -12.5 ' } }) fireEvent.click(screen.getByTestId('add-property-confirm')) expect(onAddProperty).toHaveBeenCalledWith('estimate', -12.5) }) it('shows date picker trigger when date type selected', { timeout: 15_000 }, () => { openAddPropertyForm() fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Date/ })) expect(screen.getByTestId('add-property-date-trigger')).toBeInTheDocument() expect(screen.getByText('Pick a date\u2026')).toBeInTheDocument() }) it('shows status dropdown when status type selected', { timeout: 15_000 }, () => { openAddPropertyForm() fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' }) fireEvent.click(screen.getByRole('option', { name: /Status/ })) expect(screen.getByTestId('add-property-status-trigger')).toBeInTheDocument() }) it('shows text input for text and url types', () => { openAddPropertyForm() // Default mode is text expect(screen.getByPlaceholderText('Value')).toBeInTheDocument() }) }) })