From ff4939fb2927be43da0943f3380cb58a25ac33ca Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 14 May 2026 09:55:20 +0200 Subject: [PATCH] fix: recover procedure editor schema errors --- ...torTransformErrorRecoveryExtension.test.ts | 17 ++++++ ...chEditorTransformErrorRecoveryExtension.ts | 5 ++ tests/smoke/mixed-rich-text-keypress.spec.ts | 58 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/src/components/richEditorTransformErrorRecoveryExtension.test.ts b/src/components/richEditorTransformErrorRecoveryExtension.test.ts index ea622414..65674cb0 100644 --- a/src/components/richEditorTransformErrorRecoveryExtension.test.ts +++ b/src/components/richEditorTransformErrorRecoveryExtension.test.ts @@ -45,6 +45,9 @@ describe('isRecoverableEditorTransformError', () => { it('recognizes ProseMirror transform and mismatched transaction failures', () => { expect(isRecoverableEditorTransformError(transformError())).toBe(true) expect(isRecoverableEditorTransformError(new RangeError('Applying a mismatched transaction'))).toBe(true) + expect(isRecoverableEditorTransformError(new RangeError( + 'Invalid content for node blockContainer: ', + ))).toBe(true) expect(isRecoverableEditorTransformError(new Error('unrelated'))).toBe(false) }) }) @@ -74,6 +77,20 @@ describe('installRichEditorTransformErrorRecovery', () => { }) }) + it('recovers invalid-content schema transactions from mixed paragraph and list editing', () => { + const schemaError = new RangeError( + 'Invalid content for node blockContainer: ', + ) + const { currentDoc, view } = createView(schemaError) + + installRichEditorTransformErrorRecovery(view) + + expect(() => view.dispatch({ before: currentDoc })).not.toThrow() + expect(trackEvent).toHaveBeenCalledWith('rich_editor_transform_error_recovered', { + reason: 'transform_error', + }) + }) + it('keeps non-ProseMirror dispatch failures visible', () => { const { view } = createView(new Error('plugin failed')) diff --git a/src/components/richEditorTransformErrorRecoveryExtension.ts b/src/components/richEditorTransformErrorRecoveryExtension.ts index fd69012d..14e4b6c2 100644 --- a/src/components/richEditorTransformErrorRecoveryExtension.ts +++ b/src/components/richEditorTransformErrorRecoveryExtension.ts @@ -47,10 +47,15 @@ function isMismatchedTransactionError(error: unknown): boolean { return error instanceof Error && error.message.includes('Applying a mismatched transaction') } +function isInvalidContentTransactionError(error: unknown): boolean { + return error instanceof RangeError && error.message.startsWith('Invalid content for node ') +} + export function isRecoverableEditorTransformError(error: unknown): boolean { return error instanceof Error && ( error.name === 'TransformError' || isMismatchedTransactionError(error) + || isInvalidContentTransactionError(error) ) } diff --git a/tests/smoke/mixed-rich-text-keypress.spec.ts b/tests/smoke/mixed-rich-text-keypress.spec.ts index 5d704e47..61b7513f 100644 --- a/tests/smoke/mixed-rich-text-keypress.spec.ts +++ b/tests/smoke/mixed-rich-text-keypress.spec.ts @@ -36,6 +36,21 @@ Intro paragraph before the list. Plain paragraph after the numbered list. ` +const PROCEDURE_NOTE_CONTENT = `--- +title: Procedure +type: Procedure +status: Active +--- + +# Procedure + +Procedures are long-running processes tied to a [[responsibility|Responsibility]]. +- Status: Active +- Owner: The person responsible + +Plain follow-up paragraph after the procedure checklist. +` + let tempVaultDir: string test.beforeEach(async ({ page }, testInfo) => { @@ -203,3 +218,46 @@ test('numbered list content stays editable after autosave and re-entry', async ( expect(raw).toContain(reentryMarker) expectNoBlockContainerCrash(crashSignals) }) + +test('Procedure prose and adjacent bullet-list content stay editable through keydown edits', async ({ page }) => { + const crashSignals = collectEditorCrashSignals(page) + const markerId = Date.now() + const paragraphMarker = `procedure-paragraph-${markerId}` + const ownerMarker = `procedure-owner-${markerId}` + const cadenceMarker = `procedure-cadence-${markerId}` + + await openNote(page, 'Note B') + await toggleRawMode(page, '.cm-content') + await setRawEditorContent(page, PROCEDURE_NOTE_CONTENT) + await page.waitForTimeout(700) + await toggleRawMode(page, '.bn-editor') + + const procedureParagraph = page.locator('.bn-block-content', { + hasText: 'Procedures are long-running processes', + }).first() + await expect(procedureParagraph).toBeVisible({ timeout: 5_000 }) + await procedureParagraph.click() + await page.keyboard.press('End') + await page.keyboard.insertText(` ${paragraphMarker}`) + await page.waitForTimeout(700) + + const ownerItem = page.locator('.bn-block-content[data-content-type="bulletListItem"]', { + hasText: 'Owner: The person responsible', + }).first() + await expect(ownerItem).toBeVisible({ timeout: 5_000 }) + await ownerItem.click() + await page.keyboard.press('End') + await page.keyboard.insertText(` ${ownerMarker}`) + await page.keyboard.press('Enter') + await page.keyboard.insertText(`Cadence: Weekly ${cadenceMarker}`) + await page.waitForTimeout(900) + + expectNoBlockContainerCrash(crashSignals) + + await toggleRawMode(page, '.cm-content') + const raw = await getRawEditorContent(page) + expect(raw).toContain(paragraphMarker) + expect(raw).toContain(`- Owner: The person responsible ${ownerMarker}`) + expect(raw).toContain(`- Cadence: Weekly ${cadenceMarker}`) + expectNoBlockContainerCrash(crashSignals) +})