Compare commits

...

1 Commits

Author SHA1 Message Date
lucaronin
5b5f949c74 fix: initialize new notes with empty h1 focus 2026-04-12 00:58:11 +02:00
10 changed files with 348 additions and 25 deletions

View File

@@ -68,6 +68,12 @@
color: var(--headings-h1-color);
letter-spacing: var(--headings-h1-letter-spacing);
}
.editor__blocknote-container [data-content-type="heading"]:not([data-level])[data-is-empty-and-focused] .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before,
.editor__blocknote-container [data-content-type="heading"]:not([data-level])[data-is-only-empty-block] .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before,
.editor__blocknote-container [data-content-type="heading"][data-level="1"][data-is-empty-and-focused] .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before,
.editor__blocknote-container [data-content-type="heading"][data-level="1"][data-is-only-empty-block] .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
content: "Title" !important;
}
.editor__blocknote-container .bn-block-outer:has(> .bn-block > [data-content-type="heading"][data-level="2"]) {
margin-top: var(--headings-h2-margin-top) !important;

View File

@@ -1,4 +1,5 @@
const EDITABLE_SELECTOR = '.bn-editor [contenteditable="true"], .ProseMirror[contenteditable="true"]'
const ROOT_EDITABLE_SELECTOR = '.ProseMirror[contenteditable="true"]'
const FALLBACK_EDITABLE_SELECTOR = '.bn-editor [contenteditable="true"]'
const MAX_FOCUS_ATTEMPTS = 12
interface TiptapChain {
@@ -33,7 +34,7 @@ function selectFirstHeading(editor: FocusableEditor): void {
}
})
if (from === -1 || from >= to) return
if (from === -1 || to === -1 || from > to) return
tiptap.chain().setTextSelection({ from, to }).run()
}
@@ -42,11 +43,43 @@ function hasEditableFocus(): boolean {
return Boolean(active?.isContentEditable || active?.closest('[contenteditable="true"]'))
}
function focusEditableNode(): boolean {
const editable = document.querySelector<HTMLElement>(EDITABLE_SELECTOR)
if (!editable) return false
function canFocusWindow(): boolean {
return !navigator.userAgent.toLowerCase().includes('jsdom')
}
function focusEditableCandidate(editable: HTMLElement): boolean {
if (canFocusWindow()) {
window.focus?.()
}
editable.focus()
return true
if (hasEditableFocus()) return true
const selection = window.getSelection()
if (selection && editable.isContentEditable) {
const range = document.createRange()
range.selectNodeContents(editable)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
editable.focus()
}
return hasEditableFocus()
}
function focusEditableNode(): boolean {
const rootEditable = document.querySelector<HTMLElement>(ROOT_EDITABLE_SELECTOR)
if (rootEditable && focusEditableCandidate(rootEditable)) {
return true
}
const fallbackEditable = document.querySelector<HTMLElement>(FALLBACK_EDITABLE_SELECTOR)
if (fallbackEditable && focusEditableCandidate(fallbackEditable)) {
return true
}
return false
}
function logFocusTiming(t0: number | undefined, label: 'focus' | 'focus+select'): void {

View File

@@ -2,10 +2,10 @@ import { describe, it, expect, vi, afterEach } from 'vitest'
import { renderHook } from '@testing-library/react'
import { useEditorFocus } from './useEditorFocus'
function makeTiptapMock(hasHeading = true) {
function makeTiptapMock(hasHeading = true, headingNodeSize = 15) {
const chainResult = { setTextSelection: vi.fn().mockReturnThis(), run: vi.fn() }
const descendantsMock = vi.fn().mockImplementation((cb: (node: { type: { name: string }; nodeSize: number }, pos: number) => boolean | void) => {
if (hasHeading) cb({ type: { name: 'heading' }, nodeSize: 15 }, 2)
if (hasHeading) cb({ type: { name: 'heading' }, nodeSize: headingNodeSize }, 2)
})
return {
state: { doc: { descendants: descendantsMock } },
@@ -222,5 +222,18 @@ describe('useEditorFocus', () => {
expect(tiptap._chainResult.setTextSelection).toHaveBeenCalledWith({ from: 3, to: 16 })
expect(tiptap._chainResult.run).toHaveBeenCalled()
})
it('collapses selection to the caret for an empty H1', () => {
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })
const tiptap = makeTiptapMock(true, 2)
const { editor } = setup(true, tiptap)
window.dispatchEvent(new CustomEvent('laputa:focus-editor', { detail: { selectTitle: true } }))
expect(editor.focus).toHaveBeenCalled()
expect(tiptap.chain).toHaveBeenCalled()
expect(tiptap._chainResult.setTextSelection).toHaveBeenCalledWith({ from: 3, to: 3 })
expect(tiptap._chainResult.run).toHaveBeenCalled()
})
})
})

View File

@@ -51,6 +51,11 @@ describe('extractEditorBody', () => {
const content = '---\ntitle: My Project\ntype: Project\nstatus: Active\n---\n\n# My Project\n\n'
expect(extractEditorBody(content)).toBe('# My Project\n\n')
})
it('preserves an empty H1 for untitled-note content', () => {
const content = '---\ntype: Note\nstatus: Active\n---\n\n# \n\n'
expect(extractEditorBody(content)).toBe('# \n\n')
})
})
describe('getH1TextFromBlocks', () => {
@@ -167,7 +172,14 @@ function makeTab(path: string, title: string) {
}
}
function makeUntitledTab(path: string, title = 'Untitled Note 1') {
function makeUntitledTab(path: string, title = 'Untitled Note 1', remainder = '') {
return {
entry: { path, title, filename: `${title}.md`, type: 'Note', status: 'Active', aliases: [], isA: '' } as never,
content: `---\ntype: Note\nstatus: Active\n---\n\n# \n\n${remainder}`,
}
}
function makeBlankBodyTab(path: string, title = 'Untitled Note 1') {
return {
entry: { path, title, filename: `${title}.md`, type: 'Note', status: 'Active', aliases: [], isA: '' } as never,
content: '---\ntype: Note\nstatus: Active\n---\n',
@@ -270,7 +282,7 @@ describe('useEditorTabSwap raw mode sync', () => {
Object.defineProperty(mockEditor, 'document', { get: () => docRef.current })
const populatedTab = makeTab('a.md', 'Note A')
const untitledTab = makeUntitledTab('untitled.md')
const untitledTab = makeBlankBodyTab('untitled.md')
const { rerender } = renderHook(
({ tabs, activeTabPath, rawMode }) => useEditorTabSwap({
@@ -290,6 +302,109 @@ describe('useEditorTabSwap raw mode sync', () => {
expect(mockEditor.replaceBlocks).not.toHaveBeenCalled()
})
it('renders empty H1 untitled notes via TipTap HTML content', async () => {
vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })
const docRef = { current: blocksA as unknown[] }
const mockEditor = makeMockEditor(docRef)
Object.defineProperty(mockEditor, 'document', { get: () => docRef.current })
const populatedTab = makeTab('a.md', 'Note A')
const untitledTab = makeUntitledTab('untitled.md')
const { rerender } = renderHook(
({ tabs, activeTabPath, rawMode }) => useEditorTabSwap({
tabs, activeTabPath, editor: mockEditor as never, rawMode,
}),
{ initialProps: { tabs: [populatedTab], activeTabPath: 'a.md', rawMode: false as boolean } },
)
await act(() => new Promise(r => setTimeout(r, 0)))
mockEditor.tryParseMarkdownToBlocks.mockClear()
mockEditor.replaceBlocks.mockClear()
mockEditor._tiptapEditor.commands.setContent.mockClear()
rerender({ tabs: [untitledTab], activeTabPath: 'untitled.md', rawMode: false })
await act(() => new Promise(r => setTimeout(r, 0)))
expect(mockEditor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
expect(mockEditor.replaceBlocks).not.toHaveBeenCalled()
expect(mockEditor._tiptapEditor.commands.setContent).toHaveBeenCalledWith('<h1></h1><p></p>')
})
it('renders empty H1 typed notes with template content under the title', async () => {
vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })
const docRef = { current: blocksA as unknown[] }
const mockEditor = makeMockEditor(docRef)
mockEditor.blocksToHTMLLossy.mockReturnValue('<h2>Objective</h2><p></p>')
Object.defineProperty(mockEditor, 'document', { get: () => docRef.current })
const populatedTab = makeTab('a.md', 'Note A')
const typedUntitledTab = makeUntitledTab('untitled.md', 'Untitled Project 1', '## Objective\n\n')
const { rerender } = renderHook(
({ tabs, activeTabPath, rawMode }) => useEditorTabSwap({
tabs, activeTabPath, editor: mockEditor as never, rawMode,
}),
{ initialProps: { tabs: [populatedTab], activeTabPath: 'a.md', rawMode: false as boolean } },
)
await act(() => new Promise(r => setTimeout(r, 0)))
mockEditor.tryParseMarkdownToBlocks.mockClear()
mockEditor._tiptapEditor.commands.setContent.mockClear()
rerender({ tabs: [typedUntitledTab], activeTabPath: 'untitled.md', rawMode: false })
await act(() => new Promise(r => setTimeout(r, 0)))
expect(mockEditor.tryParseMarkdownToBlocks).toHaveBeenCalledWith('## Objective\n\n')
expect(mockEditor._tiptapEditor.commands.setContent).toHaveBeenCalledWith('<h1></h1><h2>Objective</h2><p></p>')
})
it('ignores editor change events before the pending tab swap applies a new untitled note', async () => {
vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })
const onContentChange = vi.fn()
const docRef = { current: blocksA as unknown[] }
const mockEditor = makeMockEditor(docRef)
Object.defineProperty(mockEditor, 'document', { get: () => docRef.current })
const populatedTab = makeTab('a.md', 'Note A')
const untitledTab = makeUntitledTab('untitled.md')
const { result, rerender } = renderHook(
({ tabs, activeTabPath }) => useEditorTabSwap({
tabs, activeTabPath, editor: mockEditor as never, onContentChange,
}),
{ initialProps: { tabs: [populatedTab], activeTabPath: 'a.md' } },
)
await act(() => new Promise(r => setTimeout(r, 0)))
const queued: Array<() => void> = []
vi.spyOn(globalThis, 'queueMicrotask').mockImplementation((cb: VoidFunction) => {
queued.push(cb)
})
rerender({ tabs: [untitledTab], activeTabPath: 'untitled.md' })
expect(queued).toHaveLength(1)
act(() => {
result.current.handleEditorChange()
})
expect(onContentChange).not.toHaveBeenCalled()
await act(async () => {
queued.shift()?.()
await Promise.resolve()
})
})
it('re-parses from tab.content when rawMode transitions from true to false', async () => {
vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { cb(0); return 0 })

View File

@@ -76,11 +76,20 @@ function cacheEditorState(
}
function buildFastPathBlocks(preprocessed: string): EditorBlocks | null {
if (!preprocessed.trim()) {
const trimmed = preprocessed.trim()
if (!trimmed) {
return [{ type: 'paragraph', content: [] }]
}
const h1OnlyMatch = preprocessed.trim().match(/^# (.+)$/)
if (trimmed === '#') {
return [
{ type: 'heading', props: { level: 1, textColor: 'default', backgroundColor: 'default', textAlignment: 'left' }, content: [], children: [] },
{ type: 'paragraph', content: [], children: [] },
]
}
const h1OnlyMatch = trimmed.match(/^# (.+)$/)
if (!h1OnlyMatch) return null
return [
@@ -93,6 +102,21 @@ function isBlankBodyContent(content: string): boolean {
return extractEditorBody(content).trim() === ''
}
function extractBodyRemainderAfterEmptyH1(content: string): string | null {
const body = extractEditorBody(content)
const [firstLine, secondLine, ...rest] = body.split('\n')
if (!firstLine) return null
const normalizedFirstLine = firstLine.trimEnd()
if (normalizedFirstLine !== '#' && normalizedFirstLine !== '# ') return null
if (secondLine === '') {
return rest.join('\n').trimStart()
}
return [secondLine, ...rest].join('\n').trimStart()
}
function blankParagraphBlocks(): EditorBlocks {
return [{ type: 'paragraph', content: [], children: [] }]
}
@@ -187,6 +211,40 @@ function applyBlankStateToEditor(
})
}
function applyHtmlStateToEditor(
editor: ReturnType<typeof useCreateBlockNote>,
html: string,
suppressChangeRef: MutableRefObject<boolean>,
) {
suppressChangeRef.current = true
try {
editor._tiptapEditor.commands.setContent(html)
} catch (err) {
console.error('applyHtmlStateToEditor failed:', err)
suppressChangeRef.current = false
throw err
}
queueMicrotask(() => { suppressChangeRef.current = false })
requestAnimationFrame(() => {
const scrollEl = document.querySelector('.editor__blocknote-container')
if (scrollEl) scrollEl.scrollTop = 0
})
}
async function resolveEmptyHeadingHtml(
editor: ReturnType<typeof useCreateBlockNote>,
content: string,
): Promise<string | null> {
const remainder = extractBodyRemainderAfterEmptyH1(content)
if (remainder === null) return null
if (!remainder.trim()) return '<h1></h1><p></p>'
const parsed = await parseMarkdownBlocks(editor, preProcessWikilinks(remainder))
const withWikilinks = injectWikilinks(parsed)
return `<h1></h1>${editor.blocksToHTMLLossy(withWikilinks as typeof parsed)}`
}
function findActiveTab(tabs: Tab[], activeTabPath: string | null): Tab | undefined {
return activeTabPath
? tabs.find(tab => tab.entry.path === activeTabPath)
@@ -350,8 +408,13 @@ function scheduleTabSwap(options: {
suppressChangeRef,
} = options
suppressChangeRef.current = true
const doSwap = () => {
if (prevActivePathRef.current !== targetPath) return
if (prevActivePathRef.current !== targetPath) {
suppressChangeRef.current = false
return
}
rawSwapPendingRef.current = false
if (isBlankBodyContent(activeTab.content)) {
@@ -361,6 +424,21 @@ function scheduleTabSwap(options: {
return
}
void resolveEmptyHeadingHtml(editor, activeTab.content)
.then((html) => {
if (prevActivePathRef.current !== targetPath || !html) return
applyHtmlStateToEditor(editor, html, suppressChangeRef)
requestAnimationFrame(() => signalEditorTabSwapped(targetPath))
})
.catch((err: unknown) => {
suppressChangeRef.current = false
console.error('Failed to render empty heading state:', err)
})
if (extractBodyRemainderAfterEmptyH1(activeTab.content) !== null) {
return
}
void resolveBlocksForTarget(editor, cache, targetPath, activeTab.content)
.then(({ blocks, scrollTop }) => {
if (prevActivePathRef.current !== targetPath) return
@@ -368,6 +446,7 @@ function scheduleTabSwap(options: {
requestAnimationFrame(() => signalEditorTabSwapped(targetPath))
})
.catch((err: unknown) => {
suppressChangeRef.current = false
console.error('Failed to parse/swap editor content:', err)
})
}

View File

@@ -211,6 +211,22 @@ describe('buildNoteContent', () => {
const content = buildNoteContent({ title: 'My Note', type: 'Note', status: 'Active', template: null })
expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n')
})
it('prepends an empty H1 for untitled-note creation flows', () => {
const content = buildNoteContent({ title: null, type: 'Note', status: 'Active', initialEmptyHeading: true })
expect(content).toBe('---\ntype: Note\nstatus: Active\n---\n\n# \n\n')
})
it('keeps the empty H1 ahead of templates for typed untitled notes', () => {
const content = buildNoteContent({
title: null,
type: 'Project',
status: 'Active',
template: '## Objective\n\n## Notes\n\n',
initialEmptyHeading: true,
})
expect(content).toBe('---\ntype: Project\nstatus: Active\n---\n\n# \n\n## Objective\n\n## Notes\n\n')
})
})
describe('resolveTemplate', () => {

View File

@@ -132,6 +132,21 @@ describe('buildNoteContent', () => {
const content = buildNoteContent({ title: 'P', type: 'Project', status: 'Active', template: '## Objective\n\n' })
expect(content).toContain('## Objective')
})
it('prepends an empty H1 when requested for untitled-note flows', () => {
expect(buildNoteContent({ title: null, type: 'Note', status: 'Active', initialEmptyHeading: true })).toBe('---\ntype: Note\nstatus: Active\n---\n\n# \n\n')
})
it('keeps the empty H1 before any template content', () => {
const content = buildNoteContent({
title: null,
type: 'Project',
status: 'Active',
template: '## Objective\n\n',
initialEmptyHeading: true,
})
expect(content).toBe('---\ntype: Project\nstatus: Active\n---\n\n# \n\n## Objective\n\n')
})
})
describe('resolveNewNote', () => {
@@ -247,6 +262,7 @@ describe('useNoteCreation hook', () => {
expect(addEntry).toHaveBeenCalledTimes(1)
expect(addEntry.mock.calls[0][0].title).toBe('Untitled Note 1700000000')
expect(addEntry.mock.calls[0][0].filename).toBe('untitled-note-1700000000.md')
expect(openTabWithContent.mock.calls[0][1]).toBe('---\ntype: Note\nstatus: Active\n---\n\n# \n\n')
vi.restoreAllMocks()
})
@@ -352,6 +368,7 @@ describe('useNoteCreation hook', () => {
expect(focusListener).toHaveBeenCalledTimes(1)
const event = focusListener.mock.calls[0][0] as CustomEvent
expect(event.detail.path).toMatch(/\/test\/vault\/untitled-note-\d+\.md$/)
expect(event.detail.selectTitle).toBe(true)
window.removeEventListener('laputa:focus-editor', focusListener)
})

View File

@@ -89,15 +89,23 @@ export interface NoteContentParams {
type: string
status: string | null
template?: string | null
initialEmptyHeading?: boolean
}
export function buildNoteContent({ title, type, status, template }: NoteContentParams): string {
function buildNoteBody({ template, initialEmptyHeading }: Pick<NoteContentParams, 'template' | 'initialEmptyHeading'>): string {
if (initialEmptyHeading) {
return template ? `\n# \n\n${template}` : '\n# \n\n'
}
return template ? `\n${template}` : ''
}
export function buildNoteContent({ title, type, status, template, initialEmptyHeading = false }: NoteContentParams): string {
const lines = ['---']
if (title) lines.push(`title: ${title}`)
lines.push(`type: ${type}`)
if (status) lines.push(`status: ${status}`)
lines.push('---')
const body = template ? `\n${template}` : ''
const body = buildNoteBody({ template, initialEmptyHeading })
return `${lines.join('\n')}\n${body}`
}
@@ -246,12 +254,12 @@ function createNoteImmediate(deps: ImmediateCreateDeps, type?: string): void {
const template = resolveTemplate({ entries: deps.entries, typeName: noteType })
const status = NO_STATUS_TYPES.has(noteType) ? null : 'Active'
const entry = buildNewEntry({ path: `${deps.vaultPath}/${slug}.md`, slug, title, type: noteType, status })
const content = buildNoteContent({ title: null, type: noteType, status, template })
const content = buildNoteContent({ title: null, type: noteType, status, template, initialEmptyHeading: true })
deps.openTabWithContent(entry, content)
addEntryWithMock(entry, content, deps.addEntry)
deps.trackUnsaved?.(entry.path)
deps.markContentPending?.(entry.path, content)
signalFocusEditor({ path: entry.path })
signalFocusEditor({ path: entry.path, selectTitle: true })
}
interface RelationshipCreateDeps {

View File

@@ -36,6 +36,25 @@ function untitledRow(page: Page, typeLabel: string) {
return page.getByText(new RegExp(`^Untitled ${typeLabel}(?: \\d+)?$`, 'i')).first()
}
async function expectReadyEmptyTitleHeading(page: Page): Promise<void> {
await expect.poll(async () => page.evaluate(() => {
const active = document.activeElement as HTMLElement | null
const firstBlock = document.querySelector('.bn-block-content') as HTMLElement | null
const inlineHeading = firstBlock?.querySelector('.bn-inline-content') as HTMLElement | null
return {
editorFocused: Boolean(active?.isContentEditable || active?.closest('[contenteditable="true"]')),
contentType: firstBlock?.getAttribute('data-content-type') ?? null,
placeholder: inlineHeading ? getComputedStyle(inlineHeading, '::before').content : null,
}
}), {
timeout: 5_000,
}).toEqual({
editorFocused: true,
contentType: 'heading',
placeholder: '"Title"',
})
}
test.describe('Create note crash fix', () => {
test.beforeEach(() => {
tempVaultDir = createFixtureVaultCopy()
@@ -53,6 +72,7 @@ test.describe('Create note crash fix', () => {
await selectSection(page, 'Projects')
await createNoteFromListHeader(page)
await expect(untitledRow(page, 'project')).toBeVisible({ timeout: 5_000 })
await expectReadyEmptyTitleHeading(page)
expect(errors).toEqual([])
})
@@ -66,6 +86,7 @@ test.describe('Create note crash fix', () => {
await page.locator('body').click()
await sendShortcut(page, 'n', ['Control'])
await expect(untitledRow(page, 'note')).toBeVisible({ timeout: 5_000 })
await expectReadyEmptyTitleHeading(page)
expect(errors).toEqual([])
})
@@ -78,6 +99,7 @@ test.describe('Create note crash fix', () => {
await selectSection(page, 'Events')
await createNoteFromListHeader(page)
await expect(untitledRow(page, 'event')).toBeVisible({ timeout: 5_000 })
await expectReadyEmptyTitleHeading(page)
expect(errors).toEqual([])
})
@@ -93,6 +115,7 @@ test.describe('Create note crash fix', () => {
await executeCommand(page, 'new procedure')
await expect(untitledRow(page, 'procedure')).toBeVisible({ timeout: 5_000 })
await expectReadyEmptyTitleHeading(page)
expect(errors).toEqual([])
})
})

View File

@@ -18,21 +18,17 @@ function slugifyTitle(title: string): string {
}
async function createUntitledNote(page: Page): Promise<void> {
await page.locator('body').click()
await triggerMenuCommand(page, 'file-new-note')
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+(?:-\d+)?/i, {
timeout: 5_000,
})
await expect.poll(async () => page.evaluate(() => {
const active = document.activeElement as HTMLElement | null
return Boolean(active?.isContentEditable || active?.closest('[contenteditable="true"]'))
}), {
timeout: 5_000,
}).toBe(true)
await expectReadyEmptyTitleHeading(page)
}
async function writeNewHeading(page: Page, title: string): Promise<void> {
await page.keyboard.type(`# ${title}`)
await page.keyboard.type(title)
await page.keyboard.press('Enter')
}
@@ -62,6 +58,23 @@ async function expectEditorFocused(page: Page): Promise<void> {
}).toBe(true)
}
async function expectReadyEmptyTitleHeading(page: Page): Promise<void> {
await expectEditorFocused(page)
await expect.poll(async () => page.evaluate(() => {
const firstBlock = document.querySelector('.bn-block-content') as HTMLElement | null
const inlineHeading = firstBlock?.querySelector('.bn-inline-content') as HTMLElement | null
return {
contentType: firstBlock?.getAttribute('data-content-type') ?? null,
placeholder: inlineHeading ? getComputedStyle(inlineHeading, '::before').content : null,
}
}), {
timeout: 5_000,
}).toEqual({
contentType: 'heading',
placeholder: '"Title"',
})
}
let tempVaultDir: string
test.beforeEach(async ({ page }, testInfo) => {
@@ -90,11 +103,11 @@ test('@smoke new-note H1 auto-rename keeps the editor usable and leaves no untit
for (const [index, title] of titles.entries()) {
await createUntitledNote(page)
await expectEditorFocused(page)
await writeNewHeading(page, title)
await expectActiveFilename(page, slugifyTitle(title))
await expectRenamedFile(tempVaultDir, `${slugifyTitle(title)}.md`)
await expectEditorFocused(page)
await expectFileContentContains(tempVaultDir, `${slugifyTitle(title)}.md`, `# ${title}`)
if (index === 0) {
await page.keyboard.type(' focus-probe')