fix: recover table row render errors
This commit is contained in:
@@ -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()]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user