fix: recover table row render errors

This commit is contained in:
lucaronin
2026-05-29 11:16:23 +02:00
parent 9be7049bfd
commit 9a2c025d07
5 changed files with 120 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ const state = vi.hoisted(() => ({
capturedBlockNoteOnChange: null as null | (() => void),
capturedMantineGetStyleNonce: null as null | (() => string),
blockNoteViewError: null as Error | null,
blockNoteViewErrorOnce: false,
hoverGuardMock: vi.fn(),
imageDropState: { isDragOver: false },
linkActivationMock: vi.fn(),
@@ -40,6 +41,10 @@ vi.mock('@blocknote/react', () => ({
}) => {
if (state.blockNoteViewError) {
const error = state.blockNoteViewError
if (state.blockNoteViewErrorOnce) {
state.blockNoteViewError = null
state.blockNoteViewErrorOnce = false
}
throw error
}
@@ -453,6 +458,7 @@ describe('SingleEditorView', () => {
state.capturedBlockNoteOnChange = null
state.capturedMantineGetStyleNonce = null
state.blockNoteViewError = null
state.blockNoteViewErrorOnce = false
state.imageDropState.isDragOver = false
state.personMentionCandidates = []
state.wikilinkEntriesRef.current = []
@@ -513,6 +519,34 @@ describe('SingleEditorView', () => {
}
})
it('remounts after a BlockNote table row index render error', async () => {
state.blockNoteViewError = new RangeError(
'Index 1 out of range for <tableRow(tableCell(tableParagraph("A")))>',
)
state.blockNoteViewErrorOnce = true
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
const editor = createEditor()
try {
render(
<SingleEditorView
editor={editor as never}
entries={[makeEntry()]}
onNavigateWikilink={vi.fn()}
/>,
{ wrapper: TooltipProvider, onRecoverableError: () => {} },
)
await waitFor(() => {
expect(screen.getByTestId('blocknote-view')).toBeInTheDocument()
})
expect(screen.getByTestId('blocknote-view')).toHaveAttribute('data-editable', 'true')
expect(editor.replaceBlocks).not.toHaveBeenCalled()
} finally {
consoleError.mockRestore()
}
})
it('registers the seeded BlockNote test bridge, applies column widths, and cleans it up on unmount', async () => {
const editor = createEditor()
const entries = [makeEntry()]

View File

@@ -58,6 +58,7 @@ import {
type PlainTextPasteTarget,
} from '../utils/plainTextPaste'
import {
blockNoteRenderRecoveryReason,
isRecoverableBlockNoteRenderError,
markRecoveredBlockNoteRenderError,
} from './blockNoteRenderRecovery'
@@ -130,12 +131,13 @@ class BlockNoteRenderRecoveryBoundary extends Component<{
}
componentDidCatch(error: unknown) {
if (!isRecoverableBlockNoteRenderError(error)) return
const reason = blockNoteRenderRecoveryReason(error)
if (!reason) return
if (this.state.retries >= MAX_BLOCKNOTE_RENDER_RECOVERY_RETRIES) return
const attempt = this.state.retries + 1
markRecoveredBlockNoteRenderError(error)
trackEvent('editor_render_recovered', { reason: 'block_missing_id', attempt })
trackEvent('editor_render_recovered', { reason, attempt })
this.props.onRecover?.(attempt)
this.setState(({ recoveryKey, retries }) => ({
error: null,

View File

@@ -18,6 +18,19 @@ describe('blockNoteRenderRecovery', () => {
expect(isRecoveredBlockNoteRenderError(new Error('Other render failure'), '')).toBe(false)
})
it('recognizes recovered BlockNote table row index render errors', () => {
const error = new RangeError(
'Index 1 out of range for <tableRow(tableCell(tableParagraph("A")))>',
)
expect(isRecoverableBlockNoteRenderError(error)).toBe(true)
expect(isRecoveredBlockNoteRenderError(error, '')).toBe(false)
markRecoveredBlockNoteRenderError(error)
expect(isRecoveredBlockNoteRenderError(error, '')).toBe(true)
})
it('recognizes recovered BlockNote errors from the React component stack fallback', () => {
expect(isRecoveredBlockNoteRenderError(
new Error("Block doesn't have id"),

View File

@@ -1,6 +1,11 @@
const BLOCKNOTE_MISSING_ID_ERROR = "Block doesn't have id"
const BLOCKNOTE_RECOVERY_BOUNDARY_NAME = 'BlockNoteRenderRecoveryBoundary'
const RECOVERED_BLOCKNOTE_RENDER_ERROR_MARK = '__tolariaRecoveredBlockNoteRenderError'
const BLOCKNOTE_TABLE_ROW_INDEX_ERROR = /^Index \d+ out of range for <tableRow\(/
export type BlockNoteRenderRecoveryReason =
| 'block_missing_id'
| 'table_row_index_out_of_range'
type MarkedRecoveredBlockNoteRenderError = Error & {
[RECOVERED_BLOCKNOTE_RENDER_ERROR_MARK]?: true
@@ -12,7 +17,17 @@ function hasRecoveredRenderErrorMark(error: unknown): boolean {
}
export function isRecoverableBlockNoteRenderError(error: unknown): boolean {
return error instanceof Error && error.message.includes(BLOCKNOTE_MISSING_ID_ERROR)
return blockNoteRenderRecoveryReason(error) !== null
}
export function blockNoteRenderRecoveryReason(error: unknown): BlockNoteRenderRecoveryReason | null {
if (!(error instanceof Error)) return null
if (error.message.includes(BLOCKNOTE_MISSING_ID_ERROR)) return 'block_missing_id'
if (error instanceof RangeError && BLOCKNOTE_TABLE_ROW_INDEX_ERROR.test(error.message)) {
return 'table_row_index_out_of_range'
}
return null
}
export function markRecoveredBlockNoteRenderError(error: unknown): void {