fix: recover procedure editor schema errors

This commit is contained in:
lucaronin
2026-05-14 09:55:20 +02:00
parent 3fa1d34c32
commit ff4939fb29
3 changed files with 80 additions and 0 deletions

View File

@@ -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: <paragraph("Procedures are long-running"), blockGroup(blockContainer(bulletListItem("Step")))>',
))).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: <paragraph("Procedures are long-running"), blockGroup(blockContainer(bulletListItem("Step")))>',
)
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'))

View File

@@ -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)
)
}

View File

@@ -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)
})