Files
tolaria/src/components/NoteItem.test.tsx

239 lines
7.7 KiB
TypeScript
Raw Normal View History

2026-04-08 09:03:02 +02:00
import { render, screen, fireEvent } from '@testing-library/react'
2026-04-08 21:28:28 +02:00
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { NoteItem } from './NoteItem'
2026-04-08 09:03:02 +02:00
import { makeEntry } from '../test-utils/noteListTestUtils'
2026-04-08 21:28:28 +02:00
vi.mock('../utils/url', async () => {
const actual = await vi.importActual('../utils/url') as typeof import('../utils/url')
return { ...actual, openExternalUrl: vi.fn().mockResolvedValue(undefined) }
})
const { openExternalUrl } = await import('../utils/url') as typeof import('../utils/url') & {
openExternalUrl: ReturnType<typeof vi.fn>
}
2026-04-08 09:03:02 +02:00
describe('NoteItem', () => {
2026-04-08 21:28:28 +02:00
beforeEach(() => {
openExternalUrl.mockClear()
})
2026-04-08 09:03:02 +02:00
it('renders binary files as non-clickable muted rows', () => {
const binaryEntry = makeEntry({
path: '/vault/photo.png',
filename: 'photo.png',
title: 'photo.png',
fileKind: 'binary',
2026-04-07 22:09:21 +02:00
})
2026-04-08 09:03:02 +02:00
const onClickNote = vi.fn()
2026-04-07 22:09:21 +02:00
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={binaryEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)
2026-04-07 22:09:21 +02:00
2026-04-08 09:03:02 +02:00
const item = screen.getByTestId('binary-file-item')
expect(item.className).toContain('opacity-50')
expect(item).toHaveAttribute('title', 'Cannot open this file type')
2026-04-07 22:09:21 +02:00
2026-04-08 09:03:02 +02:00
fireEvent.click(item)
expect(onClickNote).not.toHaveBeenCalled()
2026-04-07 22:09:21 +02:00
})
2026-04-08 09:03:02 +02:00
it('renders text files as clickable rows', () => {
const textEntry = makeEntry({
path: '/vault/config.yml',
filename: 'config.yml',
title: 'config.yml',
fileKind: 'text',
2026-04-07 22:09:21 +02:00
})
2026-04-08 09:03:02 +02:00
const onClickNote = vi.fn()
2026-04-07 22:09:21 +02:00
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={textEntry} isSelected={false} typeEntryMap={{}} onClickNote={onClickNote} />)
2026-04-07 22:09:21 +02:00
2026-04-08 09:03:02 +02:00
const item = screen.getByText('config.yml').closest('div')!
fireEvent.click(item)
expect(onClickNote).toHaveBeenCalled()
2026-04-07 22:09:21 +02:00
})
2026-04-08 09:03:02 +02:00
it('shows filenames instead of titles when a change status is present', () => {
const entry = makeEntry({ filename: 'my-note.md', title: 'My Note Title' })
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="modified" />)
2026-04-08 09:03:02 +02:00
expect(screen.getByText('my-note.md')).toBeInTheDocument()
expect(screen.queryByText('My Note Title')).not.toBeInTheDocument()
})
2026-04-08 09:03:02 +02:00
it('renders the correct symbol for modified files', () => {
const entry = makeEntry({ filename: 'note.md' })
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="modified" />)
2026-04-08 09:03:02 +02:00
expect(screen.getByTestId('change-status-icon').textContent).toBe('·')
})
2026-04-08 09:03:02 +02:00
it('renders the correct symbol for added files', () => {
const entry = makeEntry({ filename: 'new-note.md' })
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} changeStatus="added" />)
2026-04-08 09:03:02 +02:00
expect(screen.getByTestId('change-status-icon').textContent).toBe('+')
})
2026-04-07 21:09:06 +02:00
2026-04-08 09:03:02 +02:00
it('renders the regular title when no change status is set', () => {
const entry = makeEntry({ filename: 'note.md', title: 'My Note' })
2026-04-07 21:09:06 +02:00
2026-04-08 09:03:02 +02:00
render(<NoteItem entry={entry} isSelected={false} typeEntryMap={{}} onClickNote={vi.fn()} />)
2026-04-07 21:09:06 +02:00
2026-04-08 09:03:02 +02:00
expect(screen.getByText('My Note')).toBeInTheDocument()
expect(screen.queryByText('note.md')).not.toBeInTheDocument()
expect(screen.queryByTestId('change-status-icon')).not.toBeInTheDocument()
2026-04-07 21:09:06 +02:00
})
2026-04-08 21:28:28 +02:00
it('colors relationship chips by target type and opens the related note on Cmd+click only', () => {
const linkedProject = makeEntry({
path: '/vault/project/build-app.md',
filename: 'build-app.md',
title: 'Build App',
isA: 'Project',
})
const projectType = makeEntry({
path: '/vault/type/project.md',
filename: 'project.md',
title: 'Project',
isA: 'Type',
color: 'red',
icon: 'wrench',
})
const sourceEntry = makeEntry({
path: '/vault/note/source.md',
filename: 'source.md',
title: 'Source',
isA: 'Note',
relationships: { 'Belongs to': ['[[project/build-app]]'] },
})
const onClickNote = vi.fn()
render(
<NoteItem
entry={sourceEntry}
isSelected={false}
typeEntryMap={{ Project: projectType }}
allEntries={[sourceEntry, linkedProject, projectType]}
displayPropsOverride={['Belongs to']}
onClickNote={onClickNote}
/>,
)
const chip = screen.getByTestId('property-chip-belongs-to-0')
expect(chip).toHaveTextContent('Build App')
expect(chip.className).toContain('cursor-pointer')
expect(chip).toHaveStyle({ color: 'var(--accent-red)', backgroundColor: 'var(--accent-red-light)' })
fireEvent.click(chip)
expect(onClickNote).not.toHaveBeenCalled()
fireEvent.click(chip, { metaKey: true })
expect(onClickNote).toHaveBeenCalledWith(linkedProject, expect.objectContaining({ metaKey: true }))
})
it('falls back to the built-in type icon for relationship chips when the Type has no custom icon', () => {
const linkedTopic = makeEntry({
path: '/vault/topic/ai.md',
filename: 'ai.md',
title: 'AI',
isA: 'topic',
})
const topicType = makeEntry({
path: '/vault/type/topic.md',
filename: 'topic.md',
title: 'Topic',
isA: 'Type',
color: 'green',
icon: null,
})
const sourceEntry = makeEntry({
path: '/vault/note/source.md',
filename: 'source.md',
title: 'Source',
isA: 'Note',
relationships: { Topics: ['[[topic/ai]]'] },
})
render(
<NoteItem
entry={sourceEntry}
isSelected={false}
typeEntryMap={{ Topic: topicType, topic: topicType }}
allEntries={[sourceEntry, linkedTopic, topicType]}
displayPropsOverride={['Topics']}
onClickNote={vi.fn()}
/>,
)
const chip = screen.getByTestId('property-chip-topics-0')
expect(chip).toHaveTextContent('Ai')
expect(chip).toHaveStyle({ color: 'var(--accent-green)', backgroundColor: 'var(--accent-green-light)' })
expect(chip.querySelector('svg')).not.toBeNull()
})
2026-04-08 21:28:28 +02:00
it('opens URL chips on Cmd+click only and keeps regular clicks inert', () => {
const entry = makeEntry({
path: '/vault/note/source.md',
filename: 'source.md',
title: 'Source',
properties: { URL: 'https://example.com/docs' },
})
const onClickNote = vi.fn()
render(
<NoteItem
entry={entry}
isSelected={false}
typeEntryMap={{}}
displayPropsOverride={['URL']}
onClickNote={onClickNote}
/>,
)
const chip = screen.getByTestId('property-chip-url-0')
expect(chip).toHaveTextContent('example.com')
expect(chip.className).toContain('cursor-pointer')
expect(chip).toHaveStyle({ color: 'var(--accent-blue)', backgroundColor: 'var(--accent-blue-light)' })
fireEvent.click(chip)
expect(openExternalUrl).not.toHaveBeenCalled()
expect(onClickNote).not.toHaveBeenCalled()
fireEvent.click(chip, { metaKey: true })
expect(openExternalUrl).toHaveBeenCalledWith('https://example.com/docs')
expect(onClickNote).not.toHaveBeenCalled()
})
it('renders broken relationship chips as neutral and non-interactive', () => {
const entry = makeEntry({
path: '/vault/note/source.md',
filename: 'source.md',
title: 'Source',
relationships: { Related: ['[[missing/note]]'] },
})
const onClickNote = vi.fn()
render(
<NoteItem
entry={entry}
isSelected={false}
typeEntryMap={{}}
allEntries={[entry]}
displayPropsOverride={['Related']}
onClickNote={onClickNote}
/>,
)
const chip = screen.getByTestId('property-chip-related-0')
expect(chip).toHaveTextContent('Note')
expect(chip.className).not.toContain('cursor-pointer')
fireEvent.click(chip, { metaKey: true })
expect(onClickNote).not.toHaveBeenCalled()
expect(openExternalUrl).not.toHaveBeenCalled()
})
2026-04-07 21:09:06 +02:00
})