fix(note-list): keep status dot steady while typing
This commit is contained in:
@@ -31,18 +31,24 @@ export function getTypeIcon(isA: string | null, customIcon?: string | null): Com
|
||||
return (isA && TYPE_ICON_MAP[isA]) || FileText
|
||||
}
|
||||
|
||||
const NOTE_STATUS_DOT: Record<string, { color: string; testId: string; title: string }> = {
|
||||
type VisibleNoteStatus = Exclude<NoteStatus, 'clean'>
|
||||
|
||||
const NOTE_STATUS_DOT: Record<VisibleNoteStatus, { color: string; testId: string; title: string }> = {
|
||||
pendingSave: { color: 'var(--accent-green)', testId: 'pending-save-indicator', title: 'Saving to disk…' },
|
||||
unsaved: { color: 'var(--accent-green)', testId: 'unsaved-indicator', title: 'Saving to disk…' },
|
||||
new: { color: 'var(--accent-green)', testId: 'new-indicator', title: 'New (uncommitted)' },
|
||||
modified: { color: 'var(--accent-orange)', testId: 'modified-indicator', title: 'Modified (uncommitted)' },
|
||||
}
|
||||
|
||||
function StatusDot({ noteStatus }: { noteStatus: NoteStatus }) {
|
||||
function hasStatusDot(noteStatus: NoteStatus): noteStatus is VisibleNoteStatus {
|
||||
return noteStatus !== 'clean'
|
||||
}
|
||||
|
||||
function StatusDot({ noteStatus }: { noteStatus: VisibleNoteStatus }) {
|
||||
const dot = NOTE_STATUS_DOT[noteStatus]
|
||||
if (!dot) return null
|
||||
return (
|
||||
<span
|
||||
className={`mr-1.5 inline-block align-middle${noteStatus === 'pendingSave' ? ' tab-status-pulse' : ''}`}
|
||||
className="mr-1.5 inline-block align-middle"
|
||||
style={{ width: 6, height: 6, borderRadius: '50%', background: dot.color, verticalAlign: 'middle' }}
|
||||
data-testid={dot.testId}
|
||||
title={dot.title}
|
||||
@@ -278,7 +284,7 @@ function NoteTitleRow({
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('truncate pr-5 text-[13px]', isBinary ? 'text-muted-foreground' : 'text-foreground', isSelected && !isBinary ? 'font-semibold' : 'font-medium')}>
|
||||
{noteStatus !== 'clean' && !isBinary && <StatusDot noteStatus={noteStatus} />}
|
||||
{hasStatusDot(noteStatus) && !isBinary && <StatusDot noteStatus={noteStatus} />}
|
||||
<NoteTitleIcon icon={entry.icon} size={15} className="mr-1" testId="note-title-icon" />
|
||||
{entry.title}
|
||||
{!isBinary && <StateBadge archived={entry.archived} />}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NoteList } from './NoteList'
|
||||
import { makeEntry, makeIndexedEntry, mockEntries, renderNoteList } from '../test-utils/noteListTestUtils'
|
||||
import type { NoteStatus } from '../types'
|
||||
|
||||
describe('NoteList status indicators', () => {
|
||||
it('shows a modified indicator for modified notes', () => {
|
||||
@@ -40,6 +41,27 @@ describe('NoteList status indicators', () => {
|
||||
expect(screen.getAllByTestId('new-indicator')).toHaveLength(1)
|
||||
expect(screen.queryByTestId('modified-indicator')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the green indicator steady while a new note is edited and saved', () => {
|
||||
const targetPath = mockEntries[0].path
|
||||
const getNoteStatus = (noteStatus: NoteStatus) => (path: string) => path === targetPath ? noteStatus : 'clean'
|
||||
const { props, rerender } = renderNoteList({ getNoteStatus: getNoteStatus('new') })
|
||||
const rerenderWithStatus = (nextStatus: NoteStatus) => {
|
||||
rerender(<NoteList {...props} getNoteStatus={getNoteStatus(nextStatus)} />)
|
||||
}
|
||||
const expectSteadyIndicator = (testId: string) => {
|
||||
const indicator = screen.getByTestId(testId)
|
||||
expect(indicator).not.toHaveClass('tab-status-pulse')
|
||||
}
|
||||
|
||||
expectSteadyIndicator('new-indicator')
|
||||
rerenderWithStatus('unsaved')
|
||||
expectSteadyIndicator('unsaved-indicator')
|
||||
rerenderWithStatus('pendingSave')
|
||||
expectSteadyIndicator('pending-save-indicator')
|
||||
rerenderWithStatus('new')
|
||||
expectSteadyIndicator('new-indicator')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NoteList virtualized datasets', () => {
|
||||
|
||||
85
tests/smoke/note-status-indicator-stability.spec.ts
Normal file
85
tests/smoke/note-status-indicator-stability.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog'
|
||||
import {
|
||||
createFixtureVaultCopy,
|
||||
openFixtureVaultDesktopHarness,
|
||||
removeFixtureVaultCopy,
|
||||
} from '../helpers/fixtureVault'
|
||||
import { triggerMenuCommand } from './testBridge'
|
||||
|
||||
const STATUS_DOT_SELECTOR = [
|
||||
'[data-testid="new-indicator"]',
|
||||
'[data-testid="unsaved-indicator"]',
|
||||
'[data-testid="pending-save-indicator"]',
|
||||
].join(',')
|
||||
|
||||
interface StatusDotSample {
|
||||
className: string | null
|
||||
testId: string | null
|
||||
}
|
||||
|
||||
interface StatusDotSampleWindow {
|
||||
__noteStatusDotSampler?: number
|
||||
__noteStatusDotSamples?: StatusDotSample[]
|
||||
}
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
async function startStatusDotSampler(page: Page) {
|
||||
await page.evaluate((selector) => {
|
||||
const sampleWindow = window as typeof window & StatusDotSampleWindow
|
||||
sampleWindow.__noteStatusDotSamples = []
|
||||
const sample = () => {
|
||||
const dot = document.querySelector(selector) as HTMLElement | null
|
||||
sampleWindow.__noteStatusDotSamples?.push({
|
||||
className: dot?.className ?? null,
|
||||
testId: dot?.dataset.testid ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
sample()
|
||||
sampleWindow.__noteStatusDotSampler = window.setInterval(sample, 100)
|
||||
}, STATUS_DOT_SELECTOR)
|
||||
}
|
||||
|
||||
async function stopStatusDotSampler(page: Page): Promise<StatusDotSample[]> {
|
||||
return page.evaluate(() => {
|
||||
const sampleWindow = window as typeof window & StatusDotSampleWindow
|
||||
if (sampleWindow.__noteStatusDotSampler !== undefined) {
|
||||
window.clearInterval(sampleWindow.__noteStatusDotSampler)
|
||||
sampleWindow.__noteStatusDotSampler = undefined
|
||||
}
|
||||
return sampleWindow.__noteStatusDotSamples ?? []
|
||||
})
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testInfo.setTimeout(60_000)
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir)
|
||||
})
|
||||
|
||||
test.afterEach(() => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('new note status indicator stays steady through typing and autosave', async ({ page }) => {
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.fileNewNote)
|
||||
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+/i, {
|
||||
timeout: 5_000,
|
||||
})
|
||||
await expect(page.locator(STATUS_DOT_SELECTOR).first()).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await startStatusDotSampler(page)
|
||||
await page.locator('.bn-editor').click()
|
||||
await page.keyboard.type('The sidebar status dot should stay steady while this note is edited. ', {
|
||||
delay: 25,
|
||||
})
|
||||
await page.waitForTimeout(1_800)
|
||||
|
||||
const samples = await stopStatusDotSampler(page)
|
||||
expect(samples.length).toBeGreaterThan(0)
|
||||
expect(samples.filter((sample) => sample.testId === null)).toEqual([])
|
||||
expect(samples.some((sample) => sample.testId === 'unsaved-indicator')).toBe(true)
|
||||
expect(samples.filter((sample) => sample.className?.includes('tab-status-pulse'))).toEqual([])
|
||||
})
|
||||
Reference in New Issue
Block a user