import { render, screen, fireEvent } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { NoteItem } from './NoteItem' import { makeEntry } from '../test-utils/noteListTestUtils' 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 } describe('NoteItem', () => { beforeEach(() => { openExternalUrl.mockClear() }) it('renders binary files as non-clickable muted rows', () => { const binaryEntry = makeEntry({ path: '/vault/photo.png', filename: 'photo.png', title: 'photo.png', fileKind: 'binary', }) const onClickNote = vi.fn() render() const item = screen.getByTestId('binary-file-item') expect(item.className).toContain('opacity-50') expect(item).toHaveAttribute('title', 'Cannot open this file type') fireEvent.click(item) expect(onClickNote).not.toHaveBeenCalled() }) it('renders text files as clickable rows', () => { const textEntry = makeEntry({ path: '/vault/config.yml', filename: 'config.yml', title: 'config.yml', fileKind: 'text', }) const onClickNote = vi.fn() render() const item = screen.getByText('config.yml').closest('div')! fireEvent.click(item) expect(onClickNote).toHaveBeenCalled() }) it('shows filenames instead of titles when a change status is present', () => { const entry = makeEntry({ filename: 'my-note.md', title: 'My Note Title' }) render() expect(screen.getByText('my-note.md')).toBeInTheDocument() expect(screen.queryByText('My Note Title')).not.toBeInTheDocument() }) it('renders the correct symbol for modified files', () => { const entry = makeEntry({ filename: 'note.md' }) render() expect(screen.getByTestId('change-status-icon').textContent).toBe('ยท') }) it('renders the correct symbol for added files', () => { const entry = makeEntry({ filename: 'new-note.md' }) render() expect(screen.getByTestId('change-status-icon').textContent).toBe('+') }) it('renders the regular title when no change status is set', () => { const entry = makeEntry({ filename: 'note.md', title: 'My Note' }) render() expect(screen.getByText('My Note')).toBeInTheDocument() expect(screen.queryByText('note.md')).not.toBeInTheDocument() expect(screen.queryByTestId('change-status-icon')).not.toBeInTheDocument() }) 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( , ) 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( , ) 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() }) 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( , ) 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( , ) 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() }) })