Files
tolaria/src/components/BreadcrumbBar.test.tsx
Luca Rossi b05992e2b1 feat: new note creation — unsaved/in-memory state before first save (#112)
* feat: add pendingSave status indicator for new note creation

Track disk write state during note creation with a pulsing green dot
on tab and breadcrumb bar. The pending state resolves to 'new' once
the file is written to disk, giving users clear feedback during the
async save.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add tests for pendingSave status lifecycle

Cover resolveNoteStatus priority, createAndPersist callbacks,
TabBar pulsing dot indicator, and BreadcrumbBar "Saving…" text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add design frames for new note creation pending save state

Two frames in design/new-note-creation.pen:
- Pending save: pulsing green dot, italic title
- Saved: static green dot, normal title

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* design: add new-note-creation frames (pending save + after first save)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:11:31 +00:00

118 lines
4.0 KiB
TypeScript

import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { BreadcrumbBar } from './BreadcrumbBar'
import type { VaultEntry } from '../types'
const baseEntry: VaultEntry = {
path: '/vault/note/test.md',
filename: 'test.md',
title: 'Test Note',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
owner: null,
cadence: null,
archived: false,
trashed: false,
trashedAt: null,
modifiedAt: 1700000000,
createdAt: null,
fileSize: 100,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
}
const archivedEntry: VaultEntry = {
...baseEntry,
archived: true,
}
const trashedEntry: VaultEntry = {
...baseEntry,
trashed: true,
trashedAt: Date.now() / 1000 - 86400 * 5,
}
const defaultProps = {
wordCount: 100,
noteStatus: 'clean' as const,
showDiffToggle: false,
diffMode: false,
diffLoading: false,
onToggleDiff: vi.fn(),
}
describe('BreadcrumbBar — trash/restore', () => {
it('shows trash button for non-trashed note', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onTrash={vi.fn()} onRestore={vi.fn()} />)
expect(screen.getByTitle('Move to trash (Cmd+Delete)')).toBeInTheDocument()
expect(screen.queryByTitle('Restore from trash')).not.toBeInTheDocument()
})
it('shows restore button for trashed note', () => {
render(<BreadcrumbBar entry={trashedEntry} {...defaultProps} onTrash={vi.fn()} onRestore={vi.fn()} />)
expect(screen.getByTitle('Restore from trash')).toBeInTheDocument()
expect(screen.queryByTitle('Move to trash (Cmd+Delete)')).not.toBeInTheDocument()
})
it('calls onTrash when trash button is clicked', () => {
const onTrash = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onTrash={onTrash} onRestore={vi.fn()} />)
fireEvent.click(screen.getByTitle('Move to trash (Cmd+Delete)'))
expect(onTrash).toHaveBeenCalledOnce()
})
it('calls onRestore when restore button is clicked', () => {
const onRestore = vi.fn()
render(<BreadcrumbBar entry={trashedEntry} {...defaultProps} onTrash={vi.fn()} onRestore={onRestore} />)
fireEvent.click(screen.getByTitle('Restore from trash'))
expect(onRestore).toHaveBeenCalledOnce()
})
})
describe('BreadcrumbBar — pending save indicator', () => {
it('shows "Saving…" text when noteStatus is pendingSave', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} noteStatus={'pendingSave'} />)
expect(screen.getByText('Saving…')).toBeInTheDocument()
})
it('does not show "Saving…" text for clean status', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} noteStatus={'clean'} />)
expect(screen.queryByText('Saving…')).not.toBeInTheDocument()
})
})
describe('BreadcrumbBar — archive/unarchive', () => {
it('shows archive button for non-archived note', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onArchive={vi.fn()} onUnarchive={vi.fn()} />)
expect(screen.getByTitle('Archive (Cmd+E)')).toBeInTheDocument()
expect(screen.queryByTitle('Unarchive (Cmd+E)')).not.toBeInTheDocument()
})
it('shows unarchive button for archived note', () => {
render(<BreadcrumbBar entry={archivedEntry} {...defaultProps} onArchive={vi.fn()} onUnarchive={vi.fn()} />)
expect(screen.getByTitle('Unarchive (Cmd+E)')).toBeInTheDocument()
expect(screen.queryByTitle('Archive (Cmd+E)')).not.toBeInTheDocument()
})
it('calls onArchive when archive button is clicked', () => {
const onArchive = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onArchive={onArchive} />)
fireEvent.click(screen.getByTitle('Archive (Cmd+E)'))
expect(onArchive).toHaveBeenCalledOnce()
})
it('calls onUnarchive when unarchive button is clicked', () => {
const onUnarchive = vi.fn()
render(<BreadcrumbBar entry={archivedEntry} {...defaultProps} onUnarchive={onUnarchive} />)
fireEvent.click(screen.getByTitle('Unarchive (Cmd+E)'))
expect(onUnarchive).toHaveBeenCalledOnce()
})
})