fix: prevent stale raw content after note switch

This commit is contained in:
lucaronin
2026-04-13 14:23:20 +02:00
parent 487e8d0352
commit 0a52ae6c1d
2 changed files with 116 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { render, screen, fireEvent, act } from '@testing-library/react'
import type { ComponentProps } from 'react'
import { describe, it, expect, vi } from 'vitest'
@@ -13,6 +13,7 @@ const mockEditor = vi.hoisted(() => ({
onMount: vi.fn((cb: () => void) => { cb(); return () => {} }),
prosemirrorView: {} as Record<string, unknown>,
blocksToHTMLLossy: vi.fn(() => ''),
blocksToMarkdownLossy: vi.fn(() => '# Test Project\n\nThis is a test note with some words to count.\n'),
_tiptapEditor: { commands: { setContent: vi.fn() } },
focus: vi.fn(),
setTextCursorPosition: vi.fn(),
@@ -55,6 +56,7 @@ vi.mock('@blocknote/mantine/style.css', () => ({}))
import { Editor } from './Editor'
import { applyPendingRawExitContent } from './editorRawModeSync'
import type { VaultEntry } from '../types'
import { bindVaultConfigStore, resetVaultConfigStore } from '../utils/vaultConfigStore'
type EditorComponentProps = ComponentProps<typeof Editor>
@@ -254,6 +256,101 @@ describe('Editor', () => {
mockEditor.replaceBlocks.mockClear()
mockEditor.insertBlocks.mockClear()
})
it('does not apply note A raw content to note B when raw mode closes during note B load', async () => {
resetVaultConfigStore()
bindVaultConfigStore(
{
zoom: null,
view_mode: null,
editor_mode: null,
tag_colors: null,
status_colors: null,
property_display_modes: null,
inbox: null,
},
vi.fn(),
)
const rawToggleRef = { current: (() => {}) as () => void }
const onContentChange = vi.fn()
const noteA = {
entry: {
...mockEntry,
path: '/vault/project/note-a.md',
filename: 'note-a.md',
title: 'Note A',
},
content: '---\ntitle: Note A\n---\n\n# Note A\n\nAlpha body.',
}
const noteBEntry: VaultEntry = {
...mockEntry,
path: '/vault/project/note-b.md',
filename: 'note-b.md',
title: 'Note B',
}
const noteB = {
entry: noteBEntry,
content: '---\ntitle: Note B\n---\n\n# Note B\n\nBravo body.',
}
const { rerender } = render(
<Editor
{...defaultProps}
tabs={[noteA]}
activeTabPath={noteA.entry.path}
entries={[noteA.entry, noteBEntry]}
onContentChange={onContentChange}
rawToggleRef={rawToggleRef}
/>,
)
await vi.waitFor(() => {
expect(typeof rawToggleRef.current).toBe('function')
})
await act(async () => {
await rawToggleRef.current()
})
onContentChange.mockClear()
mockEditor.tryParseMarkdownToBlocks.mockClear()
mockEditor.replaceBlocks.mockClear()
rerender(
<Editor
{...defaultProps}
tabs={[noteA]}
activeTabPath={noteB.entry.path}
entries={[noteA.entry, noteBEntry]}
onContentChange={onContentChange}
rawToggleRef={rawToggleRef}
/>,
)
await act(async () => {
await rawToggleRef.current()
})
expect(onContentChange).not.toHaveBeenCalledWith(noteB.entry.path, noteA.content)
rerender(
<Editor
{...defaultProps}
tabs={[noteA, noteB]}
activeTabPath={noteB.entry.path}
entries={[noteA.entry, noteB.entry]}
onContentChange={onContentChange}
rawToggleRef={rawToggleRef}
/>,
)
await vi.waitFor(() => {
expect(mockEditor.tryParseMarkdownToBlocks).toHaveBeenCalledWith(expect.stringContaining('Note B'))
})
expect(mockEditor.tryParseMarkdownToBlocks).not.toHaveBeenCalledWith(expect.stringContaining('Note A'))
resetVaultConfigStore()
})
})
describe('applyPendingRawExitContent', () => {

View File

@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback, useState, memo } from 'react'
import { useRef, useEffect, useLayoutEffect, useCallback, useState, memo } from 'react'
import { useEditorTabSwap } from '../hooks/useEditorTabSwap'
import { useCreateBlockNote } from '@blocknote/react'
import '@blocknote/mantine/style.css'
@@ -159,9 +159,25 @@ function useRawModeWithFlush(
onContentChange?: (path: string, content: string) => void,
) {
const rawLatestContentRef = useRef<string | null>(null)
const rawBufferPathRef = useRef<string | null>(null)
const [pendingRawExitContent, setPendingRawExitContent] = useState<PendingRawExitContent | null>(null)
const [rawModeContentOverride, setRawModeContentOverride] = useState<PendingRawExitContent | null>(null)
useLayoutEffect(() => {
if (!activeTabPath) {
rawLatestContentRef.current = null
rawBufferPathRef.current = null
return
}
if (rawBufferPathRef.current === activeTabPath) {
return
}
rawLatestContentRef.current = activeTabContent
rawBufferPathRef.current = activeTabContent === null ? null : activeTabPath
}, [activeTabContent, activeTabPath])
const handleFlushPending = useCallback(async () => {
const syncedContent = syncActiveTabIntoRawBuffer({
editor,
@@ -182,6 +198,7 @@ function useRawModeWithFlush(
onContentChange,
}))
setRawModeContentOverride(null)
rawBufferPathRef.current = null
rawLatestContentRef.current = null
}, [activeTabPath, onContentChange])