Files
orca/tests/e2e/helpers/markdown-inline-image.ts
fb9ba4b681 fix(editor): make markdown images inline so a paragraph stays schema-valid (#19746)
* fix(editor): make markdown images inline so a paragraph stays schema-valid

Image was registered as a block node while paragraph is content:'inline*',
but the markdown pipeline nests an inline image as a paragraph child.
Schema.nodeFromJSON does not validate content, so the editor built a
schema-invalid document that rendered fine and threw on the first step
that reassembled the paragraph - i.e. on the user's next keystroke.

Report 0e46c048 (1.4.198, macOS): RangeError "Invalid content for node
paragraph" from checkContent via Node.replace, tearing down the
editor.rich-markdown boundary.

Register Image as inline and override paragraph's parseMarkdown so a lone
image is not hoisted out of its paragraph. Also fixes the same crash class
reachable through details/summary. Markdown output is byte-identical.

* fix(editor): keep a fenced code block intact when an image is inserted into it

Making the image node inline meant it could no longer be fitted into
codeBlock (content:'text*', marks:''), so inserting one with the cursor
inside a fence made ProseMirror close the block at the insertion point:
the remaining code escaped as plain prose and the language attribute was
lost, and autosave wrote that markdown to the user's file. The pre-fix
block image split the fence into two intact blocks instead.

Resolve the insert content against the target position: when an inline
image cannot be fitted where the caret sits, wrap it in a paragraph so
ProseMirror splits the block and both halves keep their ``` fencing and
language. Prose insertion is unchanged. Every production insert path now
shares that resolution - the toolbar picker, the slash command and the
clipboard-screenshot paste through insertRichMarkdownImageFromPath, plus
the GitHub/GitLab composer's image-URL insert - each with a regression
test.

Also guard the unchecked cast of Paragraph.config.parseMarkdown: a Tiptap
upgrade that drops the field would otherwise turn every paragraph parse
into a TypeError and take the whole editor down, instead of degrading to
parseInline.

Four of the new round-trip cases asserted only on getMarkdown(), which
walks the document without running NodeType.checkContent and so emits
byte-identical output from a schema-invalid document - they passed on the
pre-fix code. roundTripMarkdown now runs doc.check(), the list-item and
table-cell case performs a real edit, and the standalone-image case types
beside the image. All twelve cases now fail on the merge-base.

Adds an Electron e2e spec driving the real renderer: a paragraph image and
a toggle-summary image each survive a keystroke, and Bold over a selection
spanning the image keeps it.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <neil@stably.ai>
2026-09-10 17:42:50 -07:00

72 lines
2.2 KiB
TypeScript

import { mkdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
const ERROR_BOUNDARY_TEXT = 'The rich markdown editor hit an unexpected error'
const SCHEMA_ERROR_SIGNATURE = 'Invalid content for node'
// A 22x22 PNG dot, small enough to keep inline with the surrounding text.
const INLINE_DOT_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAOklEQVR42mOoMGL4TwvMQHOD//dQB48aTKbB6IBigwmBwWUwsWDUYPINHnqpgqYZZLQQoqrBQ6bOAwDparQl4qEv0wAAAABJRU5ErkJggg=='
export const INLINE_IMAGE_FIXTURE_DIRECTORY = '.orca-e2e-markdown-inline-image'
export const INLINE_IMAGE_PARAGRAPH_MARKDOWN = [
'# Inline image crash repro',
'',
'Some text ![alt](inline-dot.png) more text',
''
].join('\n')
export const INLINE_IMAGE_DETAILS_MARKDOWN = [
'# Details summary inline image repro',
'',
'<details class="orca-details" open>',
'<summary>Toggle ![alt](inline-dot.png) label</summary>',
'',
'Body',
'',
'</details>',
''
].join('\n')
export function writeInlineImageAsset(rootPath: string): void {
const directory = path.join(rootPath, INLINE_IMAGE_FIXTURE_DIRECTORY)
mkdirSync(directory, { recursive: true })
writeFileSync(
path.join(directory, 'inline-dot.png'),
Buffer.from(INLINE_DOT_PNG_BASE64, 'base64')
)
}
/**
* The schema RangeError is thrown inside EditorView.dispatch, outside React's
* render phase, so no error boundary observes it — it only surfaces as a page
* error. Collect both signals.
*/
export function collectRichMarkdownPageErrors(page: Page): string[] {
const pageErrors: string[] = []
page.on('pageerror', (error) => pageErrors.push(`${error.name}: ${error.message}`))
page.on('console', (message) => {
if (message.type() === 'error') {
pageErrors.push(message.text())
}
})
return pageErrors
}
export async function expectNoRichMarkdownSchemaCrash(
page: Page,
pageErrors: string[]
): Promise<void> {
expect(
pageErrors.filter((entry) => entry.includes(SCHEMA_ERROR_SIGNATURE)),
'no schema RangeError may be raised'
).toEqual([])
await expect(
page.getByText(ERROR_BOUNDARY_TEXT),
'rich markdown error boundary must not trip'
).toHaveCount(0)
}