From 2b1f4395b33e73030acf587cb312b7e826d52967 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 6 Jun 2026 03:57:21 +0200 Subject: [PATCH] fix: recover null editor transform errors --- ...hEditorTransformErrorRecoveryExtension.test.ts | 15 +++++++++++++++ .../richEditorTransformErrorRecoveryExtension.ts | 14 +++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/components/richEditorTransformErrorRecoveryExtension.test.ts b/src/components/richEditorTransformErrorRecoveryExtension.test.ts index e6aceec3..601307cd 100644 --- a/src/components/richEditorTransformErrorRecoveryExtension.test.ts +++ b/src/components/richEditorTransformErrorRecoveryExtension.test.ts @@ -16,6 +16,10 @@ function transformError(message = 'Invalid transform') { return error } +function nullFragmentAppendError(message = "null is not an object (evaluating 'o.fillBefore(e).append')") { + return new TypeError(message) +} + function createView(error?: Error) { const currentDoc = { eq: vi.fn((candidate: unknown) => candidate === currentDoc), @@ -94,6 +98,10 @@ describe('isRecoverableEditorTransformError', () => { expect(isRecoverableEditorTransformError(new Error( 'Block with ID 6c1c3bb4-e218-4f00-aaf5-40606852d286 not found', ))).toBe(true) + expect(isRecoverableEditorTransformError(nullFragmentAppendError())).toBe(true) + expect(isRecoverableEditorTransformError(new TypeError( + "Cannot read properties of null (reading 'append')", + ))).toBe(false) expect(isRecoverableEditorTransformError(new RangeError( 'Index 1 out of range for ', ))).toBe(false) @@ -213,6 +221,13 @@ describe('installRichEditorTransformErrorRecovery', () => { ) }) + it('repairs null fragment append failures from invalid document model fills', () => { + expectDocumentRepairRecovery( + nullFragmentAppendError(), + 'null_fragment_append', + ) + }) + it('recovers stale block-reference transactions from toolbar actions', () => { const { currentDoc, view } = createView(new Error( 'Block with ID 6c1c3bb4-e218-4f00-aaf5-40606852d286 not found', diff --git a/src/components/richEditorTransformErrorRecoveryExtension.ts b/src/components/richEditorTransformErrorRecoveryExtension.ts index 4b93c9e4..50bebf71 100644 --- a/src/components/richEditorTransformErrorRecoveryExtension.ts +++ b/src/components/richEditorTransformErrorRecoveryExtension.ts @@ -48,6 +48,7 @@ type RecoveryReason = | 'invalid_block_join' | 'invalid_insertion_depth' | 'mismatched_transaction' + | 'null_fragment_append' | 'stale_block_reference' | 'stale_transaction' | 'table_position_out_of_range' @@ -95,6 +96,13 @@ function isInvalidBlockJoinError(error: unknown): boolean { return isTransformError(error) && /^Cannot join (blockGroup|tableCell) onto blockContainer$/.test(error.message) } +function isNullFragmentAppendError(error: unknown): boolean { + if (!(error instanceof TypeError)) return false + + const details = `${error.message}\n${error.stack ?? ''}` + return details.includes('fillBefore') && details.includes('.append') +} + export function isStaleBlockReferenceError(error: unknown): boolean { return error instanceof Error && /^Block with ID .+ not found$/.test(error.message) } @@ -113,6 +121,7 @@ const RECOVERABLE_EDITOR_ERROR_PREDICATES = [ isTransformError, isMismatchedTransactionError, isRecoverableRangeError, + isNullFragmentAppendError, isStaleBlockReferenceError, ] @@ -130,12 +139,15 @@ function recoveryReason( if (isStaleBlockReferenceError(error)) return 'stale_block_reference' if (isInvalidBlockJoinError(error)) return 'invalid_block_join' if (isInvalidInsertionDepthError(error)) return 'invalid_insertion_depth' + if (isNullFragmentAppendError(error)) return 'null_fragment_append' if (isTablePositionOutOfRangeError(error)) return 'table_position_out_of_range' return 'transform_error' } function shouldRepairEditorDocument(error: unknown): boolean { - return isRecoverableRangeError(error) || isInvalidBlockJoinError(error) + return isRecoverableRangeError(error) + || isInvalidBlockJoinError(error) + || isNullFragmentAppendError(error) } export const reportRecoveredEditorTransformError = (reason: RecoveryReason, error: unknown): void => {