2026-04-16 10:25:56 +02:00
|
|
|
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
2026-04-08 09:03:02 +02:00
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { openNoteListPropertiesPicker } from './note-list/noteListPropertiesEvents'
|
|
|
|
|
import {
|
|
|
|
|
allSelection,
|
|
|
|
|
makeEntry,
|
|
|
|
|
makeTypeDefinition,
|
|
|
|
|
mockEntries,
|
|
|
|
|
renderNoteList,
|
|
|
|
|
} from '../test-utils/noteListTestUtils'
|
|
|
|
|
|
2026-04-10 18:31:51 +02:00
|
|
|
function makeBookTypeEntries(
|
|
|
|
|
displayProps: string[] = [],
|
|
|
|
|
entryOverrides: Parameters<typeof makeEntry>[0] = {},
|
|
|
|
|
) {
|
|
|
|
|
return [
|
|
|
|
|
makeTypeDefinition('Book', displayProps),
|
|
|
|
|
makeEntry({
|
|
|
|
|
path: '/vault/book.md',
|
|
|
|
|
filename: 'book.md',
|
|
|
|
|
title: 'Book Note',
|
|
|
|
|
isA: 'Book',
|
|
|
|
|
createdAt: 1700000000,
|
|
|
|
|
...entryOverrides,
|
|
|
|
|
}),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 09:03:02 +02:00
|
|
|
describe('NoteList rendering', () => {
|
|
|
|
|
it('shows an empty state when there are no entries', () => {
|
|
|
|
|
renderNoteList({ entries: [] })
|
|
|
|
|
expect(screen.getByText('No notes found')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders all entries in the all-notes view', () => {
|
|
|
|
|
renderNoteList()
|
|
|
|
|
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('filters section groups by type', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'sectionGroup', type: 'Person' } })
|
|
|
|
|
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('supports event sections', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'sectionGroup', type: 'Event' } })
|
|
|
|
|
expect(screen.getByText('Kickoff Meeting')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('supports project sections', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'sectionGroup', type: 'Project' } })
|
|
|
|
|
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('passes the selected type when creating a note from a type section', () => {
|
|
|
|
|
const { onCreateNote } = renderNoteList({ selection: { kind: 'sectionGroup', type: 'Project' } })
|
|
|
|
|
fireEvent.click(screen.getByTitle('Create new note'))
|
|
|
|
|
expect(onCreateNote).toHaveBeenCalledWith('Project')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('creates an untyped note from all notes', () => {
|
|
|
|
|
const { onCreateNote } = renderNoteList()
|
|
|
|
|
fireEvent.click(screen.getByTitle('Create new note'))
|
|
|
|
|
expect(onCreateNote).toHaveBeenCalledWith(undefined)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('pins the current entity and shows grouped children', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
expect(screen.getAllByText('Build Laputa App').length).toBeGreaterThanOrEqual(1)
|
|
|
|
|
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Children')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Related to')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows referenced-by groups for topic entities', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[4] } })
|
|
|
|
|
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Referenced By')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('toggles the search input from the header action', () => {
|
|
|
|
|
renderNoteList()
|
|
|
|
|
expect(screen.queryByPlaceholderText('Search notes...')).not.toBeInTheDocument()
|
|
|
|
|
fireEvent.click(screen.getByTitle('Search notes'))
|
|
|
|
|
expect(screen.getByPlaceholderText('Search notes...')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('filters by a case-insensitive search query', () => {
|
|
|
|
|
renderNoteList()
|
|
|
|
|
fireEvent.click(screen.getByTitle('Search notes'))
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('Search notes...'), { target: { value: 'facebook' } })
|
|
|
|
|
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('sorts entries by last modified descending by default', () => {
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: [
|
|
|
|
|
{ ...mockEntries[0], modifiedAt: 1000, title: 'Oldest' },
|
|
|
|
|
{ ...mockEntries[1], modifiedAt: 3000, title: 'Newest', path: '/p2' },
|
|
|
|
|
{ ...mockEntries[2], modifiedAt: 2000, title: 'Middle', path: '/p3' },
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const titles = screen.getAllByText(/Oldest|Newest|Middle/).map((element) => element.textContent)
|
|
|
|
|
expect(titles).toEqual(['Newest', 'Middle', 'Oldest'])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('hides standalone status badges inside note rows', () => {
|
|
|
|
|
renderNoteList()
|
|
|
|
|
expect(screen.queryByText('Active')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows search and create actions in the header instead of a count badge', () => {
|
|
|
|
|
renderNoteList()
|
|
|
|
|
expect(screen.getByTitle('Search notes')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByTitle('Create new note')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-13 23:33:46 +02:00
|
|
|
it('uses breadcrumbs-like button styling for note-list header actions', () => {
|
2026-04-12 22:29:36 +02:00
|
|
|
renderNoteList({
|
|
|
|
|
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High' } }),
|
|
|
|
|
selection: { kind: 'filter', filter: 'inbox' },
|
|
|
|
|
inboxNoteListProperties: null,
|
|
|
|
|
onUpdateInboxNoteListProperties: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const buttons = [
|
|
|
|
|
screen.getByTitle('Search notes'),
|
|
|
|
|
screen.getByTitle('Customize Inbox columns'),
|
|
|
|
|
screen.getByTitle('Create new note'),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for (const button of buttons) {
|
|
|
|
|
expect(button).toHaveAttribute('data-variant', 'ghost')
|
2026-04-13 23:33:46 +02:00
|
|
|
expect(button).toHaveClass(
|
|
|
|
|
'!h-auto',
|
|
|
|
|
'!w-auto',
|
|
|
|
|
'!min-w-0',
|
|
|
|
|
'!rounded-none',
|
|
|
|
|
'!p-0',
|
|
|
|
|
'!text-muted-foreground',
|
|
|
|
|
'hover:!bg-transparent',
|
|
|
|
|
'hover:!text-foreground',
|
|
|
|
|
)
|
|
|
|
|
expect(button).not.toHaveAttribute('tabindex', '-1')
|
2026-04-12 22:29:36 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-08 09:03:02 +02:00
|
|
|
it('shows backlinks from outgoing links in entity view', () => {
|
|
|
|
|
const entriesWithBacklink = mockEntries.map((entry) =>
|
|
|
|
|
entry.path === mockEntries[2].path ? { ...entry, outgoingLinks: ['Build Laputa App'] } : entry,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: entriesWithBacklink,
|
|
|
|
|
selection: { kind: 'entity', entry: mockEntries[0] },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Backlinks')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Matteo Cellini')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('collapses and expands entity groups', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByText('Children'))
|
|
|
|
|
expect(screen.queryByText('Facebook Ads Strategy')).not.toBeInTheDocument()
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByText('Children'))
|
|
|
|
|
expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows the entity snippet in the prominent card', () => {
|
|
|
|
|
renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
expect(screen.getByText('Build a personal knowledge management app.')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('shows the inbox customize-columns action and falls back to type-defined chips', () => {
|
|
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High', Owner: 'Luca' } }),
|
2026-04-08 09:03:02 +02:00
|
|
|
selection: { kind: 'filter', filter: 'inbox' },
|
|
|
|
|
inboxNoteListProperties: null,
|
|
|
|
|
onUpdateInboxNoteListProperties: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTitle('Customize Inbox columns')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('High')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Luca')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-16 02:18:43 +02:00
|
|
|
it('shows the all-notes customize-columns action and falls back to type-defined chips', () => {
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High', Owner: 'Luca' } }),
|
|
|
|
|
selection: allSelection,
|
|
|
|
|
allNotesNoteListProperties: null,
|
|
|
|
|
onUpdateAllNotesNoteListProperties: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTitle('Customize All Notes columns')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('High')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('Luca')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-16 10:25:56 +02:00
|
|
|
it('opens the all-notes column picker as a searchable combobox and saves new columns', async () => {
|
2026-04-16 02:18:43 +02:00
|
|
|
const onUpdateAllNotesNoteListProperties = vi.fn()
|
|
|
|
|
const archivedOwnerEntry = makeEntry({
|
|
|
|
|
path: '/vault/book-archive.md',
|
|
|
|
|
filename: 'book-archive.md',
|
|
|
|
|
title: 'Archived Book',
|
|
|
|
|
isA: 'Book',
|
|
|
|
|
archived: true,
|
|
|
|
|
properties: { Owner: 'Luca' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: [
|
|
|
|
|
...makeBookTypeEntries(['Priority'], { properties: { Priority: 'High' } }),
|
|
|
|
|
archivedOwnerEntry,
|
|
|
|
|
],
|
|
|
|
|
selection: allSelection,
|
|
|
|
|
allNotesNoteListProperties: null,
|
|
|
|
|
onUpdateAllNotesNoteListProperties,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
openNoteListPropertiesPicker('all')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('list-properties-popover')).toBeInTheDocument()
|
2026-04-16 17:03:50 +02:00
|
|
|
expect(screen.getByTestId('list-properties-popover')).toHaveClass('overflow-hidden')
|
2026-04-16 10:25:56 +02:00
|
|
|
expect(screen.getByTestId('list-properties-scroll-area')).toBeInTheDocument()
|
2026-04-16 17:03:50 +02:00
|
|
|
expect(screen.getByTestId('list-properties-scroll-area')).toHaveClass('overflow-y-auto')
|
2026-04-16 02:18:43 +02:00
|
|
|
expect(screen.getByRole('checkbox', { name: 'Priority' })).toBeChecked()
|
|
|
|
|
expect(screen.getByRole('checkbox', { name: 'Owner' })).toBeInTheDocument()
|
|
|
|
|
|
2026-04-16 10:25:56 +02:00
|
|
|
const combobox = screen.getByRole('combobox', { name: 'Search note-list properties' })
|
|
|
|
|
await waitFor(() => expect(combobox).toHaveFocus())
|
|
|
|
|
|
|
|
|
|
fireEvent.change(combobox, { target: { value: 'Owner' } })
|
|
|
|
|
expect(screen.getByRole('checkbox', { name: 'Owner' })).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByRole('checkbox', { name: 'Priority' })).not.toBeInTheDocument()
|
|
|
|
|
|
2026-04-16 02:18:43 +02:00
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Owner' }))
|
|
|
|
|
expect(onUpdateAllNotesNoteListProperties).toHaveBeenCalledWith(['Priority', 'Owner'])
|
2026-04-16 10:25:56 +02:00
|
|
|
|
|
|
|
|
fireEvent.keyDown(combobox, { key: 'Escape' })
|
|
|
|
|
await waitFor(() => expect(screen.queryByTestId('list-properties-popover')).not.toBeInTheDocument())
|
2026-04-16 02:18:43 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-08 09:03:02 +02:00
|
|
|
it('opens the inbox column picker from the global event and saves new columns', () => {
|
|
|
|
|
const onUpdateInboxNoteListProperties = vi.fn()
|
|
|
|
|
|
|
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High', Owner: 'Luca' } }),
|
2026-04-08 09:03:02 +02:00
|
|
|
selection: { kind: 'filter', filter: 'inbox' },
|
|
|
|
|
inboxNoteListProperties: null,
|
|
|
|
|
onUpdateInboxNoteListProperties,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
openNoteListPropertiesPicker('inbox')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('list-properties-popover')).toBeInTheDocument()
|
2026-04-16 10:25:56 +02:00
|
|
|
expect(screen.getByRole('combobox', { name: 'Search note-list properties' })).toBeInTheDocument()
|
2026-04-08 09:03:02 +02:00
|
|
|
expect(screen.getByRole('checkbox', { name: 'Priority' })).toBeChecked()
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: 'Owner' }))
|
|
|
|
|
expect(onUpdateInboxNoteListProperties).toHaveBeenCalledWith(['Priority', 'Owner'])
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-08 20:56:12 +02:00
|
|
|
it('shows status in the type column picker when at least one note has it set', () => {
|
|
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries([], { status: 'Active' }),
|
2026-04-08 20:56:12 +02:00
|
|
|
selection: { kind: 'sectionGroup', type: 'Book' },
|
|
|
|
|
onUpdateTypeSort: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
openNoteListPropertiesPicker('type')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('list-properties-popover')).toBeInTheDocument()
|
2026-04-16 10:25:56 +02:00
|
|
|
expect(screen.getByRole('combobox', { name: 'Search note-list properties' })).toBeInTheDocument()
|
2026-04-08 20:56:12 +02:00
|
|
|
expect(screen.getByRole('checkbox', { name: 'status' })).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps blank statuses out of the type column picker', () => {
|
|
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries([], { status: '', properties: { Owner: 'Luca' } }),
|
2026-04-08 20:56:12 +02:00
|
|
|
selection: { kind: 'sectionGroup', type: 'Book' },
|
|
|
|
|
onUpdateTypeSort: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
openNoteListPropertiesPicker('type')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByTestId('list-properties-popover')).toBeInTheDocument()
|
|
|
|
|
expect(screen.getByRole('checkbox', { name: 'Owner' })).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByRole('checkbox', { name: 'status' })).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders status as a note-list chip when a type displays it', () => {
|
2026-04-10 18:31:51 +02:00
|
|
|
renderNoteList({
|
|
|
|
|
entries: makeBookTypeEntries(['status'], { status: 'Active' }),
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Book' },
|
|
|
|
|
})
|
2026-04-08 20:56:12 +02:00
|
|
|
|
2026-04-10 18:31:51 +02:00
|
|
|
const chip = screen.getByTestId('property-chip-status-0')
|
|
|
|
|
expect(chip).toHaveTextContent('• Active')
|
|
|
|
|
expect(chip).toHaveStyle({ backgroundColor: 'var(--accent-green-light)', color: 'var(--accent-green)' })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('auto-detects status-like property values in note-list chips', () => {
|
2026-04-08 20:56:12 +02:00
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries(['Phase'], { properties: { Phase: 'Draft' } }),
|
2026-04-08 20:56:12 +02:00
|
|
|
selection: { kind: 'sectionGroup', type: 'Book' },
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-10 18:31:51 +02:00
|
|
|
const chip = screen.getByTestId('property-chip-phase-0')
|
|
|
|
|
expect(chip).toHaveTextContent('• Draft')
|
|
|
|
|
expect(chip).toHaveStyle({ backgroundColor: 'var(--accent-yellow-light)', color: 'var(--accent-yellow)' })
|
2026-04-08 20:56:12 +02:00
|
|
|
})
|
|
|
|
|
|
2026-04-10 18:31:51 +02:00
|
|
|
it('keeps unknown status values on neutral note-list chip styling', () => {
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: makeBookTypeEntries(['status'], { status: 'Needs Review' }),
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Book' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const chip = screen.getByTestId('property-chip-status-0')
|
|
|
|
|
expect(chip).toHaveTextContent('• Needs Review')
|
|
|
|
|
expect(chip.getAttribute('style')).toBeNull()
|
|
|
|
|
})
|
2026-04-08 09:03:02 +02:00
|
|
|
|
2026-04-10 18:31:51 +02:00
|
|
|
it('uses inbox overrides when configured', () => {
|
2026-04-08 09:03:02 +02:00
|
|
|
renderNoteList({
|
2026-04-10 18:31:51 +02:00
|
|
|
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High', Owner: 'Luca' } }),
|
2026-04-08 09:03:02 +02:00
|
|
|
selection: { kind: 'filter', filter: 'inbox' },
|
|
|
|
|
inboxNoteListProperties: ['Owner'],
|
|
|
|
|
onUpdateInboxNoteListProperties: () => undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Luca')).toBeInTheDocument()
|
|
|
|
|
expect(screen.queryByText('High')).not.toBeInTheDocument()
|
|
|
|
|
})
|
2026-04-08 21:28:28 +02:00
|
|
|
|
|
|
|
|
it('Cmd+clicks relationship chips through the note list without triggering the row click', () => {
|
|
|
|
|
const projectType = makeTypeDefinition('Project')
|
|
|
|
|
const taskType = makeTypeDefinition('Task', ['Belongs to'])
|
|
|
|
|
const projectEntry = makeEntry({
|
|
|
|
|
path: '/vault/project/build-app.md',
|
|
|
|
|
filename: 'build-app.md',
|
|
|
|
|
title: 'Build App',
|
|
|
|
|
isA: 'Project',
|
|
|
|
|
createdAt: 1700000000,
|
|
|
|
|
})
|
|
|
|
|
const taskEntry = makeEntry({
|
|
|
|
|
path: '/vault/task/write-tests.md',
|
|
|
|
|
filename: 'write-tests.md',
|
|
|
|
|
title: 'Write tests',
|
|
|
|
|
isA: 'Task',
|
|
|
|
|
relationships: { 'Belongs to': ['[[project/build-app]]'] },
|
|
|
|
|
createdAt: 1700000001,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList({
|
|
|
|
|
entries: [projectType, taskType, projectEntry, taskEntry],
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Task' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const chip = screen.getByTestId('property-chip-belongs-to-0')
|
|
|
|
|
|
|
|
|
|
fireEvent.click(chip)
|
|
|
|
|
expect(onReplaceActiveTab).not.toHaveBeenCalled()
|
|
|
|
|
expect(onSelectNote).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
|
|
fireEvent.click(chip, { metaKey: true })
|
|
|
|
|
expect(onSelectNote).toHaveBeenCalledWith(projectEntry)
|
|
|
|
|
expect(onReplaceActiveTab).not.toHaveBeenCalled()
|
|
|
|
|
})
|
2026-04-08 09:03:02 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('NoteList click behavior', () => {
|
|
|
|
|
it('opens the current tab on a regular click', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList()
|
|
|
|
|
fireEvent.click(screen.getByText('Build Laputa App'))
|
|
|
|
|
expect(onReplaceActiveTab).toHaveBeenCalledWith(mockEntries[0])
|
|
|
|
|
expect(onSelectNote).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens a new tab on Cmd+Click', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList()
|
|
|
|
|
fireEvent.click(screen.getByText('Build Laputa App'), { metaKey: true })
|
|
|
|
|
expect(onSelectNote).toHaveBeenCalledWith(mockEntries[0])
|
|
|
|
|
expect(onReplaceActiveTab).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens a new tab on Ctrl+Click', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList()
|
|
|
|
|
fireEvent.click(screen.getByText('Build Laputa App'), { ctrlKey: true })
|
|
|
|
|
expect(onSelectNote).toHaveBeenCalledWith(mockEntries[0])
|
|
|
|
|
expect(onReplaceActiveTab).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('supports Cmd+Click on the entity pinned card', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
const titles = screen.getAllByText('Build Laputa App')
|
|
|
|
|
fireEvent.click(titles[titles.length - 1], { metaKey: true })
|
|
|
|
|
expect(onSelectNote).toHaveBeenCalledWith(mockEntries[0])
|
|
|
|
|
expect(onReplaceActiveTab).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens the current tab from the entity pinned card on regular click', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
const titles = screen.getAllByText('Build Laputa App')
|
|
|
|
|
fireEvent.click(titles[titles.length - 1])
|
|
|
|
|
expect(onReplaceActiveTab).toHaveBeenCalledWith(mockEntries[0])
|
|
|
|
|
expect(onSelectNote).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('opens child notes from entity view in the current tab', () => {
|
|
|
|
|
const { onReplaceActiveTab, onSelectNote } = renderNoteList({ selection: { kind: 'entity', entry: mockEntries[0] } })
|
|
|
|
|
fireEvent.click(screen.getByText('Facebook Ads Strategy'))
|
|
|
|
|
expect(onReplaceActiveTab).toHaveBeenCalledWith(mockEntries[1])
|
|
|
|
|
expect(onSelectNote).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('NoteList type sections', () => {
|
|
|
|
|
const typeEntry = {
|
|
|
|
|
...makeEntry({
|
|
|
|
|
path: '/Users/luca/Laputa/types/project.md',
|
|
|
|
|
filename: 'project.md',
|
|
|
|
|
title: 'Project',
|
|
|
|
|
isA: 'Type',
|
|
|
|
|
snippet: 'Defines the Project type.',
|
|
|
|
|
modifiedAt: 1700000000,
|
|
|
|
|
fileSize: 200,
|
|
|
|
|
wordCount: 50,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
const entriesWithType = [...mockEntries, typeEntry]
|
|
|
|
|
|
|
|
|
|
it('does not show a type note pinned card while browsing the section', () => {
|
|
|
|
|
renderNoteList({
|
|
|
|
|
entries: entriesWithType,
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Project' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('Defines the Project type.')).not.toBeInTheDocument()
|
|
|
|
|
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('renders a clickable type header that opens the type note', () => {
|
|
|
|
|
const { onReplaceActiveTab } = renderNoteList({
|
|
|
|
|
entries: entriesWithType,
|
|
|
|
|
selection: { kind: 'sectionGroup', type: 'Project' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const headerLink = screen.getByTestId('type-header-link')
|
|
|
|
|
expect(headerLink).toHaveTextContent('Project')
|
|
|
|
|
fireEvent.click(headerLink)
|
|
|
|
|
expect(onReplaceActiveTab).toHaveBeenCalledWith(typeEntry)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not render a type header outside type sections', () => {
|
|
|
|
|
renderNoteList({ entries: entriesWithType, selection: allSelection })
|
|
|
|
|
expect(screen.queryByTestId('type-header-link')).not.toBeInTheDocument()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('NoteList traffic-light padding', () => {
|
|
|
|
|
it('adds left padding when the sidebar is collapsed', () => {
|
|
|
|
|
const { container } = renderNoteList({ sidebarCollapsed: true })
|
|
|
|
|
const header = container.querySelector('.h-\\[52px\\]') as HTMLElement
|
|
|
|
|
expect(header.style.paddingLeft).toBe('80px')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not add extra left padding when the sidebar is expanded', () => {
|
|
|
|
|
const { container } = renderNoteList({ sidebarCollapsed: false })
|
|
|
|
|
const header = container.querySelector('.h-\\[52px\\]') as HTMLElement
|
|
|
|
|
expect(header.style.paddingLeft).toBe('')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('defaults to no extra padding when sidebarCollapsed is omitted', () => {
|
|
|
|
|
const { container } = renderNoteList()
|
|
|
|
|
const header = container.querySelector('.h-\\[52px\\]') as HTMLElement
|
|
|
|
|
expect(header.style.paddingLeft).toBe('')
|
|
|
|
|
})
|
|
|
|
|
})
|