2026-02-14 19:35:10 +01:00
import { render , screen , fireEvent } from '@testing-library/react'
2026-03-06 23:22:14 +01:00
import { describe , it , expect , vi } from 'vitest'
2026-02-14 18:25:11 +01:00
import { Sidebar } from './Sidebar'
2026-02-14 19:35:10 +01:00
import type { VaultEntry , SidebarSelection } from '../types'
2026-02-21 16:38:35 +01:00
2026-02-14 19:35:10 +01:00
const mockEntries : VaultEntry [ ] = [
{
path : '/vault/project/build-app.md' ,
filename : 'build-app.md' ,
title : 'Build Laputa App' ,
isA : 'Project' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : 'Active' ,
owner : 'Luca' ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:35:10 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:35:10 +01:00
fileSize : 1024 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:35:10 +01:00
} ,
{
path : '/vault/responsibility/grow-newsletter.md' ,
filename : 'grow-newsletter.md' ,
title : 'Grow Newsletter' ,
isA : 'Responsibility' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : 'Active' ,
owner : 'Luca' ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:35:10 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:35:10 +01:00
fileSize : 512 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:35:10 +01:00
} ,
{
path : '/vault/experiment/stock-screener.md' ,
filename : 'stock-screener.md' ,
title : 'Stock Screener' ,
isA : 'Experiment' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : 'Active' ,
owner : 'Luca' ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:35:10 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:35:10 +01:00
fileSize : 256 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:35:10 +01:00
} ,
{
path : '/vault/procedure/weekly-essays.md' ,
filename : 'weekly-essays.md' ,
title : 'Write Weekly Essays' ,
isA : 'Procedure' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : 'Active' ,
owner : 'Luca' ,
cadence : 'Weekly' ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:35:10 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:35:10 +01:00
fileSize : 128 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:35:10 +01:00
} ,
2026-02-14 19:42:46 +01:00
{
path : '/vault/topic/software-development.md' ,
filename : 'software-development.md' ,
title : 'Software Development' ,
isA : 'Topic' ,
aliases : [ 'Dev' , 'Coding' ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:42:46 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:42:46 +01:00
fileSize : 256 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:42:46 +01:00
} ,
{
path : '/vault/topic/trading.md' ,
filename : 'trading.md' ,
title : 'Trading' ,
isA : 'Topic' ,
aliases : [ 'Algorithmic Trading' ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-14 19:42:46 +01:00
modifiedAt : 1700000000 ,
2026-02-16 16:56:44 +01:00
createdAt : null ,
2026-02-14 19:42:46 +01:00
fileSize : 180 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-14 19:42:46 +01:00
} ,
2026-02-17 11:03:23 +01:00
{
path : '/vault/person/alice.md' ,
filename : 'alice.md' ,
title : 'Alice' ,
isA : 'Person' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-17 11:03:23 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 100 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-17 11:03:23 +01:00
} ,
{
path : '/vault/event/kickoff.md' ,
filename : 'kickoff.md' ,
title : 'Kickoff Meeting' ,
isA : 'Event' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-17 11:03:23 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 200 ,
2026-02-20 22:02:04 +01:00
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-20 22:02:04 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-17 11:03:23 +01:00
} ,
2026-02-14 19:35:10 +01:00
]
const defaultSelection : SidebarSelection = { kind : 'filter' , filter : 'all' }
2026-02-14 18:25:11 +01:00
describe ( 'Sidebar' , ( ) = > {
2026-03-06 23:22:14 +01:00
it ( 'renders top nav items (All Notes)' , ( ) = > {
2026-02-17 11:03:23 +01:00
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . getByText ( 'All Notes' ) ) . toBeInTheDocument ( )
2026-03-06 23:22:14 +01:00
expect ( screen . queryByText ( 'Favorites' ) ) . not . toBeInTheDocument ( )
2026-02-17 11:03:23 +01:00
} )
2026-02-28 20:44:35 +01:00
it ( 'renders section group headers only for types present in entries' , ( ) = > {
2026-02-14 19:35:10 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-17 11:03:23 +01:00
expect ( screen . getByText ( 'Projects' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Experiments' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Responsibilities' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Procedures' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'People' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Events' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Topics' ) ) . toBeInTheDocument ( )
2026-02-28 20:44:35 +01:00
// No entries with isA: 'Type' in mockEntries → Types section absent
expect ( screen . queryByText ( 'Types' ) ) . not . toBeInTheDocument ( )
2026-02-14 19:35:10 +01:00
} )
2026-02-21 17:33:36 +01:00
it ( 'shows entity names under their section groups after expanding' , ( ) = > {
2026-02-14 19:35:10 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
// Sections start collapsed by default — expand them first
fireEvent . click ( screen . getByLabelText ( 'Expand Projects' ) )
fireEvent . click ( screen . getByLabelText ( 'Expand Responsibilities' ) )
fireEvent . click ( screen . getByLabelText ( 'Expand Experiments' ) )
fireEvent . click ( screen . getByLabelText ( 'Expand Procedures' ) )
2026-02-14 19:35:10 +01:00
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Grow Newsletter' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Stock Screener' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Write Weekly Essays' ) ) . toBeInTheDocument ( )
} )
2026-02-21 17:33:36 +01:00
it ( 'shows People and Events items after expanding' , ( ) = > {
2026-02-14 19:35:10 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand People' ) )
fireEvent . click ( screen . getByLabelText ( 'Expand Events' ) )
2026-02-17 11:03:23 +01:00
expect ( screen . getByText ( 'Alice' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Kickoff Meeting' ) ) . toBeInTheDocument ( )
2026-02-14 19:35:10 +01:00
} )
it ( 'collapses and expands sections' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
// Start collapsed — items hidden
2026-02-14 19:35:10 +01:00
expect ( screen . queryByText ( 'Build Laputa App' ) ) . not . toBeInTheDocument ( )
2026-02-21 17:33:36 +01:00
// Expand
2026-02-17 11:03:23 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand Projects' ) )
2026-02-14 19:35:10 +01:00
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
2026-02-21 17:33:36 +01:00
// Collapse
fireEvent . click ( screen . getByLabelText ( 'Collapse Projects' ) )
expect ( screen . queryByText ( 'Build Laputa App' ) ) . not . toBeInTheDocument ( )
2026-02-14 19:35:10 +01:00
} )
it ( 'calls onSelect when clicking an entity' , ( ) = > {
const onSelect = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { onSelect } / > )
2026-02-21 17:33:36 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand Projects' ) )
2026-02-14 19:35:10 +01:00
fireEvent . click ( screen . getByText ( 'Build Laputa App' ) )
expect ( onSelect ) . toHaveBeenCalledWith ( {
kind : 'entity' ,
entry : mockEntries [ 0 ] ,
} )
} )
it ( 'calls onSelect when clicking a section header' , ( ) = > {
const onSelect = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { onSelect } / > )
2026-02-17 11:03:23 +01:00
fireEvent . click ( screen . getByText ( 'Projects' ) )
2026-02-14 19:35:10 +01:00
expect ( onSelect ) . toHaveBeenCalledWith ( {
kind : 'sectionGroup' ,
type : 'Project' ,
} )
2026-02-14 18:25:11 +01:00
} )
2026-02-14 19:38:38 +01:00
2026-03-02 01:50:46 +01:00
it ( 'expands a collapsed section when clicking its header' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// Sections start collapsed — items hidden
expect ( screen . queryByText ( 'Build Laputa App' ) ) . not . toBeInTheDocument ( )
// Click the section header text (not the chevron)
fireEvent . click ( screen . getByText ( 'Projects' ) )
// Section should now be expanded
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
} )
it ( 'collapses an expanded+selected section when clicking its header again' , ( ) = > {
const projectSelection : SidebarSelection = { kind : 'sectionGroup' , type : 'Project' }
render ( < Sidebar entries = { mockEntries } selection = { projectSelection } onSelect = { ( ) = > { } } / > )
// First click expands (starts collapsed) and selects
fireEvent . click ( screen . getByText ( 'Projects' ) )
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
// Second click: section is expanded + selected → should collapse
fireEvent . click ( screen . getByText ( 'Projects' ) )
expect ( screen . queryByText ( 'Build Laputa App' ) ) . not . toBeInTheDocument ( )
} )
it ( 'selects but keeps expanded an unselected expanded section when clicking its header' , ( ) = > {
const onSelect = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { onSelect } / > )
// Expand via chevron first
fireEvent . click ( screen . getByLabelText ( 'Expand Projects' ) )
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
// Click the header — section is expanded but not selected → should select and stay expanded
fireEvent . click ( screen . getByText ( 'Projects' ) )
expect ( onSelect ) . toHaveBeenCalledWith ( { kind : 'sectionGroup' , type : 'Project' } )
expect ( screen . getByText ( 'Build Laputa App' ) ) . toBeInTheDocument ( )
} )
2026-02-17 11:03:23 +01:00
it ( 'calls onSelect with sectionGroup for People' , ( ) = > {
2026-02-14 19:38:38 +01:00
const onSelect = vi . fn ( )
2026-02-17 11:03:23 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { onSelect } / > )
2026-02-14 19:38:38 +01:00
fireEvent . click ( screen . getByText ( 'People' ) )
expect ( onSelect ) . toHaveBeenCalledWith ( {
2026-02-17 11:03:23 +01:00
kind : 'sectionGroup' ,
type : 'Person' ,
2026-02-14 19:38:38 +01:00
} )
} )
2026-02-14 19:42:46 +01:00
2026-02-21 17:33:36 +01:00
it ( 'renders Topics section with topic entries after expanding' , ( ) = > {
2026-02-14 19:42:46 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-17 11:03:23 +01:00
expect ( screen . getByText ( 'Topics' ) ) . toBeInTheDocument ( )
2026-02-21 17:33:36 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand Topics' ) )
2026-02-14 19:42:46 +01:00
expect ( screen . getByText ( 'Software Development' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Trading' ) ) . toBeInTheDocument ( )
} )
it ( 'calls onSelect with topic kind when clicking a topic' , ( ) = > {
const onSelect = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { onSelect } / > )
2026-02-21 17:33:36 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand Topics' ) )
2026-02-14 19:42:46 +01:00
fireEvent . click ( screen . getByText ( 'Software Development' ) )
expect ( onSelect ) . toHaveBeenCalledWith ( {
kind : 'topic' ,
entry : mockEntries [ 4 ] ,
} )
} )
2026-02-20 23:50:12 +01:00
it ( 'renders + buttons for each section group when onCreateType is provided' , ( ) = > {
const onCreateType = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateType = { onCreateType } / > )
const createButtons = screen . getAllByTitle ( /^New / )
2026-02-28 20:44:35 +01:00
expect ( createButtons . length ) . toBe ( 7 ) // Projects, Experiments, Responsibilities, Procedures, People, Events, Topics (no Type entries → no Types section)
2026-02-20 23:50:12 +01:00
} )
it ( 'calls onCreateType with correct type when + button is clicked' , ( ) = > {
const onCreateType = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateType = { onCreateType } / > )
fireEvent . click ( screen . getByTitle ( 'New Project' ) )
expect ( onCreateType ) . toHaveBeenCalledWith ( 'Project' )
} )
it ( 'does not render + buttons when onCreateType is not provided' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . queryByTitle ( 'New Project' ) ) . not . toBeInTheDocument ( )
} )
2026-02-17 11:03:23 +01:00
it ( 'renders commit button even when no modified files' , ( ) = > {
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } onCommitPush = { ( ) = > { } } / > )
expect ( screen . getByText ( 'Commit & Push' ) ) . toBeInTheDocument ( )
2026-02-14 19:42:46 +01:00
} )
2026-02-17 11:03:23 +01:00
it ( 'shows badge on commit button when modified files exist' , ( ) = > {
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } modifiedCount = { 3 } onCommitPush = { ( ) = > { } } / > )
2026-02-24 14:42:06 +01:00
expect ( screen . getByText ( 'Commit & Push' ) ) . toBeInTheDocument ( )
const badges = screen . getAllByText ( '3' )
expect ( badges . length ) . toBeGreaterThanOrEqual ( 1 )
2026-02-14 19:42:46 +01:00
} )
2026-02-21 09:42:45 +01:00
2026-02-24 14:46:18 +01:00
it ( 'shows Changes nav item when modifiedCount > 0' , ( ) = > {
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } modifiedCount = { 5 } / > )
expect ( screen . getByText ( 'Changes' ) ) . toBeInTheDocument ( )
} )
it ( 'hides Changes nav item when modifiedCount is 0' , ( ) = > {
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } modifiedCount = { 0 } / > )
expect ( screen . queryByText ( 'Changes' ) ) . not . toBeInTheDocument ( )
} )
it ( 'calls onSelect with changes filter when clicking Changes' , ( ) = > {
const onSelect = vi . fn ( )
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { onSelect } modifiedCount = { 3 } / > )
fireEvent . click ( screen . getByText ( 'Changes' ) )
expect ( onSelect ) . toHaveBeenCalledWith ( { kind : 'filter' , filter : 'changes' } )
} )
2026-02-21 09:42:45 +01:00
describe ( 'dynamic custom type sections' , ( ) = > {
const entriesWithCustomTypes : VaultEntry [ ] = [
. . . mockEntries ,
{
path : '/vault/type/recipe.md' ,
filename : 'recipe.md' ,
title : 'Recipe' ,
isA : 'Type' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-21 09:42:45 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 200 ,
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-21 09:42:45 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 09:42:45 +01:00
} ,
{
path : '/vault/type/book.md' ,
filename : 'book.md' ,
title : 'Book' ,
isA : 'Type' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-21 09:42:45 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 200 ,
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-21 09:42:45 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 09:42:45 +01:00
} ,
{
path : '/vault/recipe/pasta.md' ,
filename : 'pasta.md' ,
title : 'Pasta Carbonara' ,
isA : 'Recipe' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-21 09:42:45 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 300 ,
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-21 09:42:45 +01:00
relationships : { } ,
2026-02-28 20:44:35 +01:00
icon : null ,
color : null ,
order : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-28 20:44:35 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-28 20:44:35 +01:00
} ,
{
path : '/vault/book/ddia.md' ,
filename : 'ddia.md' ,
title : 'Designing Data-Intensive Applications' ,
isA : 'Book' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
archived : false ,
trashed : false ,
trashedAt : null ,
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 400 ,
snippet : '' ,
wordCount : 0 ,
relationships : { } ,
icon : null ,
color : null ,
order : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-28 20:44:35 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 09:42:45 +01:00
} ,
]
2026-02-28 20:44:35 +01:00
it ( 'renders custom type sections derived from actual entries' , ( ) = > {
2026-02-21 09:42:45 +01:00
render ( < Sidebar entries = { entriesWithCustomTypes } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateType = { ( ) = > { } } / > )
expect ( screen . getByText ( 'Books' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Recipes' ) ) . toBeInTheDocument ( )
} )
2026-02-21 17:33:36 +01:00
it ( 'shows instances of custom types under their section after expanding' , ( ) = > {
2026-02-21 09:42:45 +01:00
render ( < Sidebar entries = { entriesWithCustomTypes } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateType = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
fireEvent . click ( screen . getByLabelText ( 'Expand Recipes' ) )
2026-02-21 09:42:45 +01:00
expect ( screen . getByText ( 'Pasta Carbonara' ) ) . toBeInTheDocument ( )
} )
it ( 'renders + button on custom type sections for creating instances' , ( ) = > {
const onCreateType = vi . fn ( )
render ( < Sidebar entries = { entriesWithCustomTypes } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateType = { onCreateType } / > )
fireEvent . click ( screen . getByTitle ( 'New Recipe' ) )
expect ( onCreateType ) . toHaveBeenCalledWith ( 'Recipe' )
} )
it ( 'calls onCreateNewType when + is clicked on Types section' , ( ) = > {
const onCreateNewType = vi . fn ( )
render ( < Sidebar entries = { entriesWithCustomTypes } selection = { defaultSelection } onSelect = { ( ) = > { } } onCreateNewType = { onCreateNewType } / > )
fireEvent . click ( screen . getByTitle ( 'New Type' ) )
expect ( onCreateNewType ) . toHaveBeenCalled ( )
} )
2026-02-28 20:44:35 +01:00
it ( 'does not show section for type with zero active entries' , ( ) = > {
// Only Type definitions exist for Book, no actual Book instances
const entriesNoBookInstance = entriesWithCustomTypes . filter ( ( e ) = > ! ( e . isA === 'Book' && e . title !== 'Book' ) )
render ( < Sidebar entries = { entriesNoBookInstance } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . queryByText ( 'Books' ) ) . not . toBeInTheDocument ( )
// Recipes still has an instance (Pasta Carbonara)
expect ( screen . getByText ( 'Recipes' ) ) . toBeInTheDocument ( )
} )
it ( 'hides type section when all entries of that type are trashed' , ( ) = > {
const entriesWithTrashedOnly : VaultEntry [ ] = [
{
path : '/vault/event/cancelled.md' , filename : 'cancelled.md' , title : 'Cancelled Event' ,
isA : 'Event' , aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null ,
cadence : null , archived : false , trashed : true , trashedAt : 1700000000 ,
modifiedAt : 1700000000 , createdAt : null , fileSize : 100 , snippet : '' , wordCount : 0 ,
2026-03-02 02:01:21 +01:00
relationships : { } , icon : null , color : null , order : null , sidebarLabel : null , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-28 20:44:35 +01:00
} ,
]
render ( < Sidebar entries = { entriesWithTrashedOnly } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . queryByText ( 'Events' ) ) . not . toBeInTheDocument ( )
} )
it ( 'shows no sections when entries list is empty' , ( ) = > {
render ( < Sidebar entries = { [ ] } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . queryByText ( 'Projects' ) ) . not . toBeInTheDocument ( )
expect ( screen . queryByText ( 'People' ) ) . not . toBeInTheDocument ( )
expect ( screen . queryByText ( 'Events' ) ) . not . toBeInTheDocument ( )
} )
2026-02-21 09:42:45 +01:00
it ( 'does not show built-in types as custom sections' , ( ) = > {
const projectTypeEntry : VaultEntry = {
path : '/vault/type/project.md' ,
filename : 'project.md' ,
title : 'Project' ,
isA : 'Type' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
2026-02-21 16:36:18 +01:00
archived : false ,
2026-02-21 16:56:48 +01:00
trashed : false ,
trashedAt : null ,
2026-02-21 09:42:45 +01:00
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 200 ,
snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-02-21 09:42:45 +01:00
relationships : { } ,
2026-02-21 13:23:10 +01:00
icon : null ,
color : null ,
2026-02-21 17:27:19 +01:00
order : null ,
2026-03-02 02:01:21 +01:00
sidebarLabel : null ,
2026-03-03 11:22:04 +01:00
template : null , sort : null ,
2026-02-25 15:04:49 +01:00
outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 09:42:45 +01:00
}
render ( < Sidebar entries = { [ . . . mockEntries , projectTypeEntry ] } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// "Projects" should appear once (the built-in section), not twice
const projectLabels = screen . getAllByText ( 'Projects' )
expect ( projectLabels . length ) . toBe ( 1 )
} )
2026-03-02 02:01:21 +01:00
it ( 'uses sidebarLabel from Type entry instead of auto-pluralization' , ( ) = > {
const entriesWithLabel : VaultEntry [ ] = [
. . . mockEntries ,
{
path : '/vault/type/news.md' , filename : 'news.md' , title : 'News' , isA : 'Type' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null ,
fileSize : 200 , snippet : '' , wordCount : 0 , relationships : { } ,
icon : null , color : null , order : null , sidebarLabel : 'News' , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-03-02 02:01:21 +01:00
} ,
{
path : '/vault/news/breaking.md' , filename : 'breaking.md' , title : 'Breaking Story' , isA : 'News' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null ,
fileSize : 300 , snippet : '' , wordCount : 0 , relationships : { } ,
icon : null , color : null , order : null , sidebarLabel : null , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-03-02 02:01:21 +01:00
} ,
]
render ( < Sidebar entries = { entriesWithLabel } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// Should show "News" (custom label), not "Newses" (auto-pluralized)
expect ( screen . getByText ( 'News' ) ) . toBeInTheDocument ( )
expect ( screen . queryByText ( 'Newses' ) ) . not . toBeInTheDocument ( )
} )
it ( 'uses sidebarLabel to override built-in type label' , ( ) = > {
const entriesWithBuiltInOverride : VaultEntry [ ] = [
. . . mockEntries ,
{
path : '/vault/type/person.md' , filename : 'person.md' , title : 'Person' , isA : 'Type' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null ,
fileSize : 200 , snippet : '' , wordCount : 0 , relationships : { } ,
icon : null , color : null , order : null , sidebarLabel : 'Contacts' , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-03-02 02:01:21 +01:00
} ,
]
render ( < Sidebar entries = { entriesWithBuiltInOverride } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . getByText ( 'Contacts' ) ) . toBeInTheDocument ( )
expect ( screen . queryByText ( 'People' ) ) . not . toBeInTheDocument ( )
} )
it ( 'falls back to auto-pluralization when sidebarLabel is null' , ( ) = > {
render ( < Sidebar entries = { entriesWithCustomTypes } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// Recipe has no sidebarLabel → should auto-pluralize to "Recipes"
expect ( screen . getByText ( 'Recipes' ) ) . toBeInTheDocument ( )
} )
2026-02-21 09:42:45 +01:00
} )
2026-02-21 16:38:35 +01:00
2026-03-06 23:22:14 +01:00
describe ( 'type visibility via visible property' , ( ) = > {
const makeTypeEntry = ( title : string , visible : boolean | null ) : VaultEntry = > ( {
path : ` /vault/type/ ${ title . toLowerCase ( ) } .md ` ,
filename : ` ${ title . toLowerCase ( ) } .md ` ,
title ,
isA : 'Type' ,
aliases : [ ] ,
belongsTo : [ ] ,
relatedTo : [ ] ,
status : null ,
owner : null ,
cadence : null ,
archived : false ,
trashed : false ,
trashedAt : null ,
modifiedAt : 1700000000 ,
createdAt : null ,
fileSize : 200 ,
snippet : '' ,
wordCount : 0 ,
relationships : { } ,
icon : null ,
color : null ,
order : null ,
sidebarLabel : null ,
template : null ,
sort : null ,
view : null ,
visible ,
outgoingLinks : [ ] ,
properties : { } ,
2026-02-21 16:38:35 +01:00
} )
2026-03-06 23:22:14 +01:00
it ( 'hides a section when its Type entry has visible: false' , ( ) = > {
const entries : VaultEntry [ ] = [
. . . mockEntries ,
makeTypeEntry ( 'Person' , false ) ,
]
render ( < Sidebar entries = { entries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . queryByText ( 'People' ) ) . not . toBeInTheDocument ( )
// Other sections should still be visible
expect ( screen . getByText ( 'Projects' ) ) . toBeInTheDocument ( )
2026-02-21 16:38:35 +01:00
} )
2026-03-06 23:22:14 +01:00
it ( 'shows a section when its Type entry has visible: true' , ( ) = > {
const entries : VaultEntry [ ] = [
. . . mockEntries ,
makeTypeEntry ( 'Person' , true ) ,
]
render ( < Sidebar entries = { entries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . getByText ( 'People' ) ) . toBeInTheDocument ( )
2026-02-21 16:38:35 +01:00
} )
2026-03-06 23:22:14 +01:00
it ( 'shows a section when its Type entry has visible: null (default)' , ( ) = > {
const entries : VaultEntry [ ] = [
. . . mockEntries ,
makeTypeEntry ( 'Person' , null ) ,
]
render ( < Sidebar entries = { entries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
expect ( screen . getByText ( 'People' ) ) . toBeInTheDocument ( )
2026-02-21 16:38:35 +01:00
} )
2026-03-06 23:22:14 +01:00
it ( 'shows a section when there is no Type entry at all (default visible)' , ( ) = > {
// mockEntries has Person instances but no Type entry for Person
2026-02-21 16:38:35 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-03-06 23:22:14 +01:00
expect ( screen . getByText ( 'People' ) ) . toBeInTheDocument ( )
2026-03-06 23:16:23 +01:00
} )
2026-03-06 23:22:14 +01:00
it ( 'hides multiple sections when their Type entries have visible: false' , ( ) = > {
const entries : VaultEntry [ ] = [
. . . mockEntries ,
makeTypeEntry ( 'Person' , false ) ,
makeTypeEntry ( 'Event' , false ) ,
]
render ( < Sidebar entries = { entries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-02-21 17:33:36 +01:00
expect ( screen . queryByText ( 'People' ) ) . not . toBeInTheDocument ( )
expect ( screen . queryByText ( 'Events' ) ) . not . toBeInTheDocument ( )
expect ( screen . getByText ( 'Projects' ) ) . toBeInTheDocument ( )
expect ( screen . getByText ( 'Topics' ) ) . toBeInTheDocument ( )
2026-02-21 16:38:35 +01:00
} )
it ( 'does not affect All Notes or other sidebar filters when sections are hidden' , ( ) = > {
2026-03-06 23:22:14 +01:00
const entries : VaultEntry [ ] = [
. . . mockEntries ,
makeTypeEntry ( 'Project' , false ) ,
makeTypeEntry ( 'Person' , false ) ,
]
render ( < Sidebar entries = { entries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
expect ( screen . getByText ( 'All Notes' ) ) . toBeInTheDocument ( )
expect ( screen . queryByText ( 'Favorites' ) ) . not . toBeInTheDocument ( )
} )
it ( 'renders a "Customize sections" button' , ( ) = > {
2026-03-06 21:33:47 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-03-06 23:22:14 +01:00
expect ( screen . getByTitle ( 'Customize sections' ) ) . toBeInTheDocument ( )
} )
2026-03-06 21:33:47 +01:00
2026-03-06 23:22:14 +01:00
it ( 'opens popover with toggle for each section when clicking customize button' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
fireEvent . click ( screen . getByTitle ( 'Customize sections' ) )
expect ( screen . getByText ( 'Show in sidebar' ) ) . toBeInTheDocument ( )
expect ( screen . getByLabelText ( 'Toggle Projects' ) ) . toBeInTheDocument ( )
expect ( screen . getByLabelText ( 'Toggle People' ) ) . toBeInTheDocument ( )
expect ( screen . getByLabelText ( 'Toggle Topics' ) ) . toBeInTheDocument ( )
} )
it ( 'calls onToggleTypeVisibility when toggling a section in the popover' , ( ) = > {
const onToggleTypeVisibility = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } onToggleTypeVisibility = { onToggleTypeVisibility } / > )
fireEvent . click ( screen . getByTitle ( 'Customize sections' ) )
fireEvent . click ( screen . getByLabelText ( 'Toggle People' ) )
expect ( onToggleTypeVisibility ) . toHaveBeenCalledWith ( 'Person' )
2026-03-06 21:33:47 +01:00
} )
2026-02-21 16:38:35 +01:00
it ( 'closes popover when clicking outside' , ( ) = > {
render (
< div >
< div data-testid = "outside" > outside < / div >
< Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / >
< / div >
)
fireEvent . click ( screen . getByTitle ( 'Customize sections' ) )
expect ( screen . getByText ( 'Show in sidebar' ) ) . toBeInTheDocument ( )
fireEvent . mouseDown ( screen . getByTestId ( 'outside' ) )
expect ( screen . queryByText ( 'Show in sidebar' ) ) . not . toBeInTheDocument ( )
} )
} )
2026-02-21 17:33:36 +01:00
describe ( 'section ordering by type order property' , ( ) = > {
const entriesWithOrder : VaultEntry [ ] = [
. . . mockEntries ,
// Type entries with order values — reversed from default
{
path : '/vault/type/project.md' , filename : 'project.md' , title : 'Project' , isA : 'Type' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
2026-02-22 11:00:07 +01:00
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null , fileSize : 200 , snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-03-02 02:01:21 +01:00
relationships : { } , icon : null , color : null , order : 5 , sidebarLabel : null , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 17:33:36 +01:00
} ,
{
path : '/vault/type/topic.md' , filename : 'topic.md' , title : 'Topic' , isA : 'Type' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
2026-02-22 11:00:07 +01:00
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null , fileSize : 200 , snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-03-02 02:01:21 +01:00
relationships : { } , icon : null , color : null , order : 0 , sidebarLabel : null , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 17:33:36 +01:00
} ,
{
path : '/vault/type/person.md' , filename : 'person.md' , title : 'Person' , isA : 'Type' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] , status : null , owner : null , cadence : null ,
2026-02-22 11:00:07 +01:00
archived : false , trashed : false , trashedAt : null , modifiedAt : 1700000000 , createdAt : null , fileSize : 200 , snippet : '' ,
2026-02-26 20:50:29 +01:00
wordCount : 0 ,
2026-03-02 02:01:21 +01:00
relationships : { } , icon : null , color : null , order : 1 , sidebarLabel : null , outgoingLinks : [ ] ,
2026-03-03 02:31:18 +01:00
properties : { } ,
2026-02-21 17:33:36 +01:00
} ,
]
it ( 'sorts sections by order from Type entries' , ( ) = > {
render ( < Sidebar entries = { entriesWithOrder } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// Get all section header labels
const headers = screen . getAllByText ( /^(Topics|People|Projects|Experiments|Responsibilities|Procedures|Events|Types)$/ )
const labels = headers . map ( ( el ) = > el . textContent )
// Topics (order: 0) and People (order: 1) should come before Projects (order: 5)
const topicsIdx = labels . indexOf ( 'Topics' )
const peopleIdx = labels . indexOf ( 'People' )
const projectsIdx = labels . indexOf ( 'Projects' )
expect ( topicsIdx ) . toBeLessThan ( projectsIdx )
expect ( peopleIdx ) . toBeLessThan ( projectsIdx )
expect ( topicsIdx ) . toBeLessThan ( peopleIdx )
} )
2026-03-02 20:06:57 +01:00
it ( 'does not render drag handle icons on section headers' , ( ) = > {
2026-02-21 17:33:36 +01:00
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
2026-03-02 20:06:57 +01:00
const dragHandles = screen . queryAllByLabelText ( /^Drag to reorder/ )
expect ( dragHandles . length ) . toBe ( 0 )
2026-02-21 17:33:36 +01:00
} )
} )
2026-03-02 21:32:41 +01:00
describe ( 'rename section via context menu' , ( ) = > {
it ( 'shows Rename section option in context menu on right-click' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
expect ( screen . getByText ( 'Rename section…' ) ) . toBeInTheDocument ( )
} )
it ( 'shows Customize icon option in context menu on right-click' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
expect ( screen . getByText ( 'Customize icon & color…' ) ) . toBeInTheDocument ( )
} )
it ( 'shows inline input when Rename section is clicked' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
fireEvent . click ( screen . getByText ( 'Rename section…' ) )
expect ( screen . getByRole ( 'textbox' , { name : 'Section name' } ) ) . toBeInTheDocument ( )
} )
it ( 'inline input is pre-filled with current label' , ( ) = > {
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
fireEvent . click ( screen . getByText ( 'Rename section…' ) )
const input = screen . getByRole ( 'textbox' , { name : 'Section name' } ) as HTMLInputElement
expect ( input . value ) . toBe ( 'Projects' )
} )
it ( 'calls onRenameSection with new name on Enter' , ( ) = > {
const onRenameSection = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } onRenameSection = { onRenameSection } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
fireEvent . click ( screen . getByText ( 'Rename section…' ) )
const input = screen . getByRole ( 'textbox' , { name : 'Section name' } )
fireEvent . change ( input , { target : { value : 'My Projects' } } )
fireEvent . keyDown ( input , { key : 'Enter' } )
expect ( onRenameSection ) . toHaveBeenCalledWith ( 'Project' , 'My Projects' )
} )
it ( 'cancels rename on Escape and hides input' , ( ) = > {
const onRenameSection = vi . fn ( )
render ( < Sidebar entries = { mockEntries } selection = { defaultSelection } onSelect = { ( ) = > { } } onRenameSection = { onRenameSection } / > )
const projectHeader = screen . getByText ( 'Projects' ) . closest ( 'div' ) !
fireEvent . contextMenu ( projectHeader )
fireEvent . click ( screen . getByText ( 'Rename section…' ) )
const input = screen . getByRole ( 'textbox' , { name : 'Section name' } )
fireEvent . keyDown ( input , { key : 'Escape' } )
expect ( onRenameSection ) . not . toHaveBeenCalled ( )
expect ( screen . queryByRole ( 'textbox' , { name : 'Section name' } ) ) . not . toBeInTheDocument ( )
} )
} )
2026-03-06 16:09:29 +01:00
it ( 'renders exactly one section for a hyphenated custom type like Monday Ideas' , ( ) = > {
const entriesWithMondayIdeas : VaultEntry [ ] = [
. . . mockEntries ,
{
path : '/vault/monday-ideas/standup-bingo.md' ,
filename : 'standup-bingo.md' ,
title : 'Standup Bingo' ,
isA : 'Monday Ideas' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] ,
status : null , owner : null , cadence : null ,
archived : false , trashed : false , trashedAt : null ,
modifiedAt : 1700000000 , createdAt : null ,
fileSize : 310 , snippet : '' , wordCount : 120 ,
relationships : { } , icon : null , color : null , order : null ,
sidebarLabel : null , template : null , sort : null , view : null ,
outgoingLinks : [ ] , properties : { } ,
} ,
{
path : '/vault/monday-ideas/theme-days.md' ,
filename : 'theme-days.md' ,
title : 'Theme Days' ,
isA : 'Monday Ideas' ,
aliases : [ ] , belongsTo : [ ] , relatedTo : [ ] ,
status : null , owner : null , cadence : null ,
archived : false , trashed : false , trashedAt : null ,
modifiedAt : 1700000000 , createdAt : null ,
fileSize : 280 , snippet : '' , wordCount : 95 ,
relationships : { } , icon : null , color : null , order : null ,
sidebarLabel : null , template : null , sort : null , view : null ,
outgoingLinks : [ ] , properties : { } ,
} ,
]
render ( < Sidebar entries = { entriesWithMondayIdeas } selection = { defaultSelection } onSelect = { ( ) = > { } } / > )
// "Monday Ideas" pluralized → "Monday Ideases" (the pluralizeType function)
const mondaySections = screen . getAllByText ( /Monday Ideas/i )
expect ( mondaySections ) . toHaveLength ( 1 )
} )
2026-02-14 18:25:11 +01:00
} )