Remove IBM Plex Mono + text-transform: uppercase from all UI labels. CSS classes .font-mono-label and .font-mono-overline now use Inter with no text-transform. Inline monospace+uppercase styles in StatusDropdown, TagsDropdown, ReferencedByPanel, AiActionCard replaced. Tailwind uppercase removed from Sidebar, CommandPalette, PulseView, CreateNoteDialog, CreateTypeDialog. Hardcoded ALL CAPS text (COLOR, ICON, TEMPLATE) converted to sentence case. Code blocks and commit hashes remain monospaced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1219 lines
40 KiB
TypeScript
1219 lines
40 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'
|
|
import { render, screen, fireEvent } 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'
|
|
|
|
// 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> = {}): 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: '',
|
|
wordCount: 0,
|
|
relationships: {},
|
|
icon: null,
|
|
color: null,
|
|
order: null,
|
|
template: null, sort: null,
|
|
outgoingLinks: [],
|
|
...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(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content="# Test\n\nSome words here"
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Type')).toBeInTheDocument()
|
|
expect(screen.getByText('Note')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders word count', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content="---\ntitle: Test\n---\nOne two three four"
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Words')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders status as colored pill', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
/>
|
|
)
|
|
// Status rendered as sentence case
|
|
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders properties from frontmatter', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Owner: 'Luca' }}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Owner')).toBeInTheDocument()
|
|
expect(screen.getByText('Luca')).toBeInTheDocument()
|
|
})
|
|
|
|
it('hides Owner with wikilink value from Properties panel', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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('hides custom field with wikilink value from Properties', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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)', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ published: false }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
// 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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ tags: ['ai', 'ml', 'deep-learning'] }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByText('ai')).toBeInTheDocument()
|
|
expect(screen.getByText('ml')).toBeInTheDocument()
|
|
expect(screen.getByText('deep-learning')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows Add property button', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Add property')).toBeInTheDocument()
|
|
})
|
|
|
|
it('opens add property form when button clicked', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
expect(screen.getByPlaceholderText('Property name')).toBeInTheDocument()
|
|
expect(screen.getByPlaceholderText('Value')).toBeInTheDocument()
|
|
expect(screen.getByTestId('add-property-type-trigger')).toBeInTheDocument()
|
|
})
|
|
|
|
it('adds property via the add form', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
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.getByTestId('add-property-confirm'))
|
|
expect(onAddProperty).toHaveBeenCalledWith('priority', 'high')
|
|
})
|
|
|
|
it('handles navigating to type via click in read-only mode', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ isA: 'Project' })}
|
|
content=""
|
|
frontmatter={{}}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
)
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('type-selector')).toBeInTheDocument()
|
|
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows available types in dropdown', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
openAndSelect('Project')
|
|
expect(onUpdateProperty).toHaveBeenCalledWith('type', 'Project')
|
|
})
|
|
|
|
it('clears type when None selected', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ isA: 'Project' })} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
openAndSelect('None')
|
|
expect(onUpdateProperty).toHaveBeenCalledWith('type', null)
|
|
})
|
|
|
|
it('shows current type even when not in available types', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ isA: 'CustomType' })} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
fireEvent.pointerDown(screen.getByRole('combobox'), { button: 0, pointerType: 'mouse' })
|
|
expect(screen.getByRole('option', { name: 'CustomType' })).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows None placeholder when entry has no type', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ isA: null })} content="" frontmatter={{}}
|
|
entries={typeEntries} onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('type-selector')).toBeInTheDocument()
|
|
expect(screen.getByText('None')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('renders modified date', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ modifiedAt: 1700000000 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Modified')).toBeInTheDocument()
|
|
})
|
|
|
|
it('opens status dropdown on click and selects a status', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
// 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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ custom_field: 'value' }}
|
|
onDeleteProperty={onDeleteProperty}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
const deleteBtn = screen.getByTitle('Delete property')
|
|
fireEvent.click(deleteBtn)
|
|
expect(onDeleteProperty).toHaveBeenCalledWith('custom_field')
|
|
})
|
|
|
|
it('coerces true/false strings to booleans on save', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ draft: 'false' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
// 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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ order: '3' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
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(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
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(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
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(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
fireEvent.click(screen.getByTestId('add-property-cancel'))
|
|
expect(screen.getByText('Add property')).toBeInTheDocument()
|
|
})
|
|
|
|
describe('editable vs read-only distinction', () => {
|
|
it('renders Info section header', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Info')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders Modified and Words in read-only Info section', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ modifiedAt: 1700000000 })}
|
|
content="---\ntitle: Test\n---\nOne two three"
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
const readOnlyRows = screen.getAllByTestId('readonly-property')
|
|
const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent)
|
|
expect(labels).toContain('Modified')
|
|
expect(labels).toContain('Words')
|
|
})
|
|
|
|
it('renders Created date in Info section', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ createdAt: 1700000000 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
const readOnlyRows = screen.getAllByTestId('readonly-property')
|
|
const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent)
|
|
expect(labels).toContain('Created')
|
|
})
|
|
|
|
it('renders file size in Info section', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ fileSize: 4300 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('Size')).toBeInTheDocument()
|
|
expect(screen.getByText('4.2 KB')).toBeInTheDocument()
|
|
})
|
|
|
|
it('shows em dash for null timestamps in Info section', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ modifiedAt: null, createdAt: null })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
// Two em dashes for null Modified and Created
|
|
const dashes = screen.getAllByText('\u2014')
|
|
expect(dashes.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
it('editable properties have hover styling via data-testid', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly', owner: 'Luca' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
onDeleteProperty={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')
|
|
})
|
|
})
|
|
|
|
it('read-only rows do not have hover styling', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
const readOnlyRows = screen.getAllByTestId('readonly-property')
|
|
readOnlyRows.forEach(row => {
|
|
expect(row.className).not.toContain('hover:bg-muted')
|
|
})
|
|
})
|
|
|
|
it('formats file sizes correctly', () => {
|
|
// Small file — bytes
|
|
const { rerender } = render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ fileSize: 500 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('500 B')).toBeInTheDocument()
|
|
|
|
// KB range
|
|
rerender(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ fileSize: 2048 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('2.0 KB')).toBeInTheDocument()
|
|
|
|
// MB range
|
|
rerender(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry({ fileSize: 1048576 })}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
expect(screen.getByText('1.0 MB')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('property row 50/50 layout', () => {
|
|
it('uses CSS grid with two equal columns on editable rows', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ url: 'https://example.com/very/long/path/that/should/be/truncated' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
const editableRows = screen.getAllByTestId('editable-property')
|
|
editableRows.forEach(row => {
|
|
expect(row.className).toContain('grid')
|
|
expect(row.className).toContain('grid-cols-2')
|
|
})
|
|
})
|
|
|
|
it('uses CSS grid with two equal columns on read-only rows', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
/>
|
|
)
|
|
const readOnlyRows = screen.getAllByTestId('readonly-property')
|
|
readOnlyRows.forEach(row => {
|
|
expect(row.className).toContain('grid')
|
|
expect(row.className).toContain('grid-cols-2')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('URL property rendering', () => {
|
|
it('renders URL values with link styling instead of plain EditableValue', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ url: 'https://example.com' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('url-link')).toBeInTheDocument()
|
|
expect(screen.getByTestId('url-link')).toHaveTextContent('https://example.com')
|
|
})
|
|
|
|
it('renders bare domain values as URL links', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ website: 'example.com' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('url-link')).toBeInTheDocument()
|
|
})
|
|
|
|
it('does not render plain text as URL link', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.queryByTestId('url-link')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows edit button on URL property', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ url: 'https://example.com' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('url-edit-btn')).toBeInTheDocument()
|
|
})
|
|
|
|
it('enters edit mode when edit button clicked on URL property', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ url: 'https://example.com' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ deadline: '2026-03-31' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('date-display')).toBeInTheDocument()
|
|
expect(screen.getByText('Mar 31, 2026')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders date trigger button', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ deadline: '2026-03-31' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
const trigger = screen.getByTestId('date-display')
|
|
expect(trigger.tagName).toBe('BUTTON')
|
|
})
|
|
|
|
it('opens calendar popover when date button clicked', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ deadline: '2026-03-31' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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 — status auto-detection', () => {
|
|
it('renders status badge for property named Status', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders status badge for known status values', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ phase: 'Draft' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('status dropdown interaction', () => {
|
|
it('closes dropdown on Escape without saving', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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' }),
|
|
]
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Status: 'Active' }}
|
|
entries={entriesWithStatuses}
|
|
onUpdateProperty={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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ published: true }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
|
expect(screen.getByText('Yes')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders boolean toggle for false values', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ published: false }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
|
expect(screen.getByText('No')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('system property filtering', () => {
|
|
it('hides trashed, trashed_at, archived, archived_at, icon from properties panel', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ trashed: true, trashed_at: '2026-01-01', archived: false, archived_at: '', icon: '📝', cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.queryByText('Trashed')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Trashed at')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Archived at')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Icon')).not.toBeInTheDocument()
|
|
// Custom property still visible
|
|
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
|
})
|
|
|
|
it('filters system properties case-insensitively', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ Trashed: true, Archived: false, Icon: '🎯', cadence: 'Daily' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.queryByText('Trashed')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('Icon')).not.toBeInTheDocument()
|
|
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
|
})
|
|
|
|
it('does not filter similar but non-matching property names', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ 'Is Trashed': true, 'archive_date': '2026-01-01' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('display-mode-trigger')).toBeInTheDocument()
|
|
})
|
|
|
|
it('opens display mode menu on trigger click', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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-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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByTestId('display-mode-trigger'))
|
|
fireEvent.click(screen.getByTestId('display-mode-option-status'))
|
|
const stored = getVaultConfig().property_display_modes as Record<string, string>
|
|
expect(stored).toBeTruthy()
|
|
expect(stored.cadence).toBe('status')
|
|
})
|
|
|
|
it('overrides rendering to status badge when status mode selected', () => {
|
|
initDisplayModeOverrides({ cadence: 'status' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ cadence: 'Weekly' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
|
})
|
|
|
|
it('renders boolean toggle for string "true" when boolean mode overridden', () => {
|
|
initDisplayModeOverrides({ draft: 'boolean' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ draft: 'true' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ draft: 'false' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
|
expect(screen.getByText('No')).toBeInTheDocument()
|
|
})
|
|
|
|
it('toggles string boolean from false to true', () => {
|
|
initDisplayModeOverrides({ draft: 'boolean' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ draft: 'false' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ due: '' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
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' })
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{ deadline: 'soon' }}
|
|
onUpdateProperty={onUpdateProperty}
|
|
/>
|
|
)
|
|
expect(screen.getByTestId('date-display')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('type-aware add property form', () => {
|
|
it('shows boolean toggle when boolean type selected', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
// 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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
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 }, () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
const keyInput = screen.getByPlaceholderText('Property name')
|
|
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('shows date picker trigger when date type selected', { timeout: 15_000 }, () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
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 }, () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
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', () => {
|
|
render(
|
|
<DynamicPropertiesPanel
|
|
entry={makeEntry()}
|
|
content=""
|
|
frontmatter={{}}
|
|
onAddProperty={onAddProperty}
|
|
/>
|
|
)
|
|
fireEvent.click(screen.getByText('Add property'))
|
|
// Default mode is text
|
|
expect(screen.getByPlaceholderText('Value')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|