fix: keep h1 typing responsive

This commit is contained in:
lucaronin
2026-04-18 11:06:55 +02:00
parent db7aaf1385
commit 4456801526
3 changed files with 82 additions and 9 deletions

View File

@@ -2,6 +2,18 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useEditorSaveWithLinks } from './useEditorSaveWithLinks'
const { startTransitionMock } = vi.hoisted(() => ({
startTransitionMock: vi.fn((callback: () => void) => callback()),
}))
vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof import('react')>()
return {
...actual,
startTransition: startTransitionMock,
}
})
const mockHandleContentChange = vi.fn()
const mockHandleSave = vi.fn()
const mockSavePendingForPath = vi.fn()
@@ -25,6 +37,7 @@ describe('useEditorSaveWithLinks', () => {
setTabs = vi.fn()
setToastMessage = vi.fn()
onAfterSave = vi.fn()
startTransitionMock.mockClear()
mockHandleContentChange.mockClear()
mockHandleSave.mockClear()
mockSavePendingForPath.mockClear()
@@ -147,6 +160,8 @@ describe('useEditorSaveWithLinks', () => {
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
isA: 'Project',
status: 'Active',
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
title: 'Note',
hasH1: false,
})
@@ -172,6 +187,8 @@ describe('useEditorSaveWithLinks', () => {
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
isA: 'Essay',
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
title: 'Note',
hasH1: false,
})
@@ -181,6 +198,8 @@ describe('useEditorSaveWithLinks', () => {
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
isA: 'Note',
})
expect(updateEntry).toHaveBeenCalledWith('/note.md', {
title: 'Note',
hasH1: false,
})
@@ -199,6 +218,20 @@ describe('useEditorSaveWithLinks', () => {
expect(updateEntry).toHaveBeenCalledWith(path, expected)
})
it('defers H1 title sync updates in a transition so typing stays responsive', () => {
const { result } = renderHookWithLinks()
act(() => {
result.current.handleContentChange('/old-title.md', '# Renamed Note\n\nBody')
})
expect(startTransitionMock).toHaveBeenCalledTimes(1)
expect(updateEntry).toHaveBeenCalledWith('/old-title.md', {
title: 'Renamed Note',
hasH1: true,
})
})
it('spreads all properties from useEditorSave onto the return value', () => {
const { result } = renderHookWithLinks()

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react'
import { startTransition, useCallback, useRef } from 'react'
import { useEditorSave } from './useEditorSave'
import { extractOutgoingLinks, extractSnippet, countWords } from '../utils/wikilinks'
import { contentToEntryPatch } from './frontmatterOps'
@@ -35,6 +35,7 @@ export function useEditorSaveWithLinks(config: {
const { handleContentChange: rawOnChange } = editor
const prevLinksKeyRef = useRef('')
const prevFmKeyRef = useRef('')
const prevTitleKeyRef = useRef('')
const handleContentChange = useCallback((path: string, content: string) => {
rawOnChange(path, content)
const links = extractOutgoingLinks(content)
@@ -45,19 +46,25 @@ export function useEditorSaveWithLinks(config: {
}
const frontmatterPatch = contentToEntryPatch(content)
const filename = path.split('/').pop() ?? path
const fmPatch = {
...frontmatterPatch,
...deriveDisplayTitleState({
content,
filename,
frontmatterTitle: typeof frontmatterPatch.title === 'string' ? frontmatterPatch.title : null,
}),
}
const titlePatch = deriveDisplayTitleState({
content,
filename,
frontmatterTitle: typeof frontmatterPatch.title === 'string' ? frontmatterPatch.title : null,
})
const fmPatch = { ...frontmatterPatch }
delete fmPatch.title
const fmKey = JSON.stringify(fmPatch)
if (fmKey !== prevFmKeyRef.current) {
prevFmKeyRef.current = fmKey
if (Object.keys(fmPatch).length > 0) updateEntry(path, fmPatch)
}
const titleKey = JSON.stringify(titlePatch)
if (titleKey !== prevTitleKeyRef.current) {
prevTitleKeyRef.current = titleKey
startTransition(() => {
updateEntry(path, titlePatch)
})
}
}, [rawOnChange, updateEntry])
return { ...editor, handleContentChange }
}

View File

@@ -65,6 +65,13 @@ function quickOpenSelectedTitle(page: Page) {
return page.getByTestId('quick-open-palette').locator('[class*="bg-accent"] span.truncate').first()
}
async function focusHeadingEnd(page: Page, title: string) {
const heading = page.getByRole('heading', { name: title, level: 1 })
await expect(heading).toBeVisible({ timeout: 5_000 })
await heading.click()
await page.keyboard.press('End')
}
test('creating an untitled draft hides the legacy title section in the editor', async ({ page }) => {
await page.locator('button[title="Create new note"]').click()
@@ -139,3 +146,29 @@ test('@smoke edited H1 titles drive note list, search, and wikilink autocomplete
const suggestionMenu = page.locator('.wikilink-menu')
await expect(suggestionMenu).toContainText(updatedTitle, { timeout: 5_000 })
})
test('@smoke rapid H1 typing stays stable while editing an existing note', async ({ page }) => {
const noteList = page.locator('[data-testid="note-list-container"]')
const firstTitle = 'Alpha Project Fast Typing Check'
const finalTitle = 'Alpha Project Fast Typing Flow'
await openNote(page, 'Alpha Project')
await focusHeadingEnd(page, 'Alpha Project')
await page.keyboard.type(' Fast Typing Check')
await expect(page.getByRole('heading', { name: firstTitle, level: 1 })).toBeVisible({ timeout: 5_000 })
for (let i = 0; i < ' Check'.length; i += 1) {
await page.keyboard.press('Backspace')
}
await page.keyboard.type(' Flow')
await page.keyboard.press('Meta+s')
await expect(page.getByRole('heading', { name: finalTitle, level: 1 })).toBeVisible({ timeout: 5_000 })
await expect(noteList.getByText(finalTitle, { exact: true })).toBeVisible({ timeout: 5_000 })
await expect(noteList.getByText('Alpha Project', { exact: true })).toHaveCount(0)
await openNote(page, 'Spring 2026')
await openNote(page, finalTitle)
await expect(page.getByRole('heading', { name: finalTitle, level: 1 })).toBeVisible({ timeout: 5_000 })
})