From 820f66a784b3ba6f27a9778bfbf378e6452946ea Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 13:34:01 +0100 Subject: [PATCH 1/6] fix(core): convert existing text via slash menu at block start --- packages/core/src/blocks/Quote/block.test.ts | 45 ++++++++++++ .../getDefaultSlashMenuItems.test.ts | 71 +++++++++++++++++++ .../getDefaultSlashMenuItems.ts | 30 +++++--- 3 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/blocks/Quote/block.test.ts create mode 100644 packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts diff --git a/packages/core/src/blocks/Quote/block.test.ts b/packages/core/src/blocks/Quote/block.test.ts new file mode 100644 index 0000000000..a25ad37907 --- /dev/null +++ b/packages/core/src/blocks/Quote/block.test.ts @@ -0,0 +1,45 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from "vite-plus/test"; + +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { insertOrUpdateBlockForSlashMenu } from "../../extensions/SuggestionMenu/getDefaultSlashMenuItems.js"; + +let editor: BlockNoteEditor | undefined; +let container: HTMLDivElement | undefined; + +afterEach(() => { + editor?.unmount(); + container?.remove(); +}); + +describe("pasting into an empty quote (#2236)", () => { + it.each(["text", "html"] as const)( + "preserves the quote when pasting %s", + (format) => { + container = document.createElement("div"); + document.body.append(container); + editor = BlockNoteEditor.create({ + initialContent: [{ id: "source", type: "paragraph" }], + }); + editor.mount(container); + editor.setTextCursorPosition("source", "start"); + insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + + if (format === "text") { + editor.pasteText("Pasted paragraph"); + } else { + editor.pasteHTML("

Pasted paragraph

"); + } + + expect(editor.document[0]).toMatchObject({ + type: "quote", + content: [{ type: "text", text: "Pasted paragraph", styles: {} }], + }); + expect( + editor.document.filter( + (block) => Array.isArray(block.content) && block.content.length > 0, + ), + ).toHaveLength(1); + }, + ); +}); diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts new file mode 100644 index 0000000000..3add266e68 --- /dev/null +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment node +import { afterEach, describe, expect, it } from "vite-plus/test"; + +import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { insertOrUpdateBlockForSlashMenu } from "./getDefaultSlashMenuItems.js"; + +let editor: BlockNoteEditor | undefined; + +afterEach(() => { + editor?._tiptapEditor.destroy(); +}); + +describe("slash-menu block conversion (#2236)", () => { + it("converts the existing paragraph to a quote at its start", () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph", content: "Existing text" }, + ], + }); + // The suggestion menu removes its slash/query before invoking the item. + editor.setTextCursorPosition("source", "start"); + + insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + + expect(editor.document).toHaveLength(1); + expect(editor.document[0]).toMatchObject({ + id: "source", + type: "quote", + content: [{ type: "text", text: "Existing text", styles: {} }], + }); + expect(editor.getTextCursorPosition().block.id).toBe("source"); + }); + + it("still inserts below when invoked at the end of a non-empty paragraph", () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph", content: "Existing text" }, + ], + }); + editor.setTextCursorPosition("source", "end"); + + insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + + expect(editor.document.map((block) => block.type)).toEqual([ + "paragraph", + "quote", + ]); + expect(editor.document[0].content).toEqual([ + { type: "text", text: "Existing text", styles: {} }, + ]); + }); + + it("still inserts non-inline blocks below existing text", () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph", content: "Existing text" }, + ], + }); + editor.setTextCursorPosition("source", "start"); + + insertOrUpdateBlockForSlashMenu(editor, { type: "divider" }); + + expect(editor.document.slice(0, 2).map((block) => block.type)).toEqual([ + "paragraph", + "divider", + ]); + expect(editor.document[0].content).toEqual([ + { type: "text", text: "Existing text", styles: {} }, + ]); + }); +}); diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 30497bb8de..7a29f22626 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -53,8 +53,8 @@ function setSelectionToNextContentEditableBlock< } } -// Checks if the current block is empty or only contains a slash, and if so, -// updates the current block instead of inserting a new one below. If the new +// Updates an empty/slash-only block, or converts an inline-content block when +// choosing another inline type at its start, instead of inserting below. If the new // block doesn't contain editable content, the cursor is moved to the next block // that does. export function insertOrUpdateBlockForSlashMenu< @@ -73,13 +73,27 @@ export function insertOrUpdateBlockForSlashMenu< let newBlock: Block; + const { selection } = editor.prosemirrorState; + // The menu has already removed the slash/query. A collapsed cursor at the + // start of the remaining text means we should convert that block in place. + // Only convert between inline-content types when no replacement content was + // supplied, so the existing text is preserved; other items insert below. + const convertsExistingText = + selection.empty && + selection.$from.parentOffset === 0 && + editor.schema.blockSchema[currentBlock.type].content === "inline" && + block.type !== undefined && + editor.schema.blockSchema[block.type].content === "inline" && + block.content === undefined; + if ( - Array.isArray(currentBlock.content) && - ((currentBlock.content.length === 1 && - isStyledTextInlineContent(currentBlock.content[0]) && - currentBlock.content[0].type === "text" && - currentBlock.content[0].text === "/") || - currentBlock.content.length === 0) + convertsExistingText || + (Array.isArray(currentBlock.content) && + ((currentBlock.content.length === 1 && + isStyledTextInlineContent(currentBlock.content[0]) && + currentBlock.content[0].type === "text" && + currentBlock.content[0].text === "/") || + currentBlock.content.length === 0)) ) { newBlock = editor.updateBlock(currentBlock, block); // We make sure to reset the cursor position to the new block as calling From bd2527fb5ff807f0341f2ec4c92fbd2e4e395c7e Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 13:51:13 +0100 Subject: [PATCH 2/6] fix(core): convert inline blocks at any caret position --- .../getDefaultSlashMenuItems.test.ts | 57 ++++++++++++------- .../getDefaultSlashMenuItems.ts | 7 +-- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts index 3add266e68..0bebeb3e02 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts @@ -1,4 +1,5 @@ // @vitest-environment node +import { TextSelection } from "@tiptap/pm/state"; import { afterEach, describe, expect, it } from "vite-plus/test"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; @@ -11,27 +12,39 @@ afterEach(() => { }); describe("slash-menu block conversion (#2236)", () => { - it("converts the existing paragraph to a quote at its start", () => { - editor = BlockNoteEditor.create({ - initialContent: [ - { id: "source", type: "paragraph", content: "Existing text" }, - ], - }); - // The suggestion menu removes its slash/query before invoking the item. - editor.setTextCursorPosition("source", "start"); + it.each([ + ["start", 0], + ["middle", 8], + ["end", 13], + ] as const)( + "converts the existing paragraph to a quote at its %s", + (_position, offset) => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph", content: "Existing text" }, + ], + }); + // The suggestion menu removes its slash/query before invoking the item. + editor.setTextCursorPosition("source", "start"); + editor.transact((tr) => + tr.setSelection( + TextSelection.create(tr.doc, tr.selection.from + offset), + ), + ); - insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); - expect(editor.document).toHaveLength(1); - expect(editor.document[0]).toMatchObject({ - id: "source", - type: "quote", - content: [{ type: "text", text: "Existing text", styles: {} }], - }); - expect(editor.getTextCursorPosition().block.id).toBe("source"); - }); + expect(editor.document).toHaveLength(1); + expect(editor.document[0]).toMatchObject({ + id: "source", + type: "quote", + content: [{ type: "text", text: "Existing text", styles: {} }], + }); + expect(editor.getTextCursorPosition().block.id).toBe("source"); + }, + ); - it("still inserts below when invoked at the end of a non-empty paragraph", () => { + it("still inserts below when the item supplies replacement content", () => { editor = BlockNoteEditor.create({ initialContent: [ { id: "source", type: "paragraph", content: "Existing text" }, @@ -39,7 +52,10 @@ describe("slash-menu block conversion (#2236)", () => { }); editor.setTextCursorPosition("source", "end"); - insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + insertOrUpdateBlockForSlashMenu(editor, { + type: "quote", + content: "New text", + }); expect(editor.document.map((block) => block.type)).toEqual([ "paragraph", @@ -48,6 +64,9 @@ describe("slash-menu block conversion (#2236)", () => { expect(editor.document[0].content).toEqual([ { type: "text", text: "Existing text", styles: {} }, ]); + expect(editor.document[1].content).toEqual([ + { type: "text", text: "New text", styles: {} }, + ]); }); it("still inserts non-inline blocks below existing text", () => { diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 7a29f22626..b17ea738d8 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -54,7 +54,7 @@ function setSelectionToNextContentEditableBlock< } // Updates an empty/slash-only block, or converts an inline-content block when -// choosing another inline type at its start, instead of inserting below. If the new +// choosing another inline type at any caret position, instead of inserting below. If the new // block doesn't contain editable content, the cursor is moved to the next block // that does. export function insertOrUpdateBlockForSlashMenu< @@ -74,13 +74,12 @@ export function insertOrUpdateBlockForSlashMenu< let newBlock: Block; const { selection } = editor.prosemirrorState; - // The menu has already removed the slash/query. A collapsed cursor at the - // start of the remaining text means we should convert that block in place. + // The menu has already removed the slash/query. With a collapsed cursor, + // choosing an inline block type converts the current block at any position. // Only convert between inline-content types when no replacement content was // supplied, so the existing text is preserved; other items insert below. const convertsExistingText = selection.empty && - selection.$from.parentOffset === 0 && editor.schema.blockSchema[currentBlock.type].content === "inline" && block.type !== undefined && editor.schema.blockSchema[block.type].content === "inline" && From 9aa04e79d675148621c42718095daff475aec7a9 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 13:56:49 +0100 Subject: [PATCH 3/6] fix(core): preserve caret during slash-menu inline conversion --- .../SuggestionMenu/getDefaultSlashMenuItems.test.ts | 2 ++ .../SuggestionMenu/getDefaultSlashMenuItems.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts index 0bebeb3e02..09b865c438 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts @@ -41,6 +41,8 @@ describe("slash-menu block conversion (#2236)", () => { content: [{ type: "text", text: "Existing text", styles: {} }], }); expect(editor.getTextCursorPosition().block.id).toBe("source"); + expect(editor.prosemirrorState.selection.empty).toBe(true); + expect(editor.prosemirrorState.selection.$from.parentOffset).toBe(offset); }, ); diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index b17ea738d8..c2c4cc6583 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -95,10 +95,12 @@ export function insertOrUpdateBlockForSlashMenu< currentBlock.content.length === 0)) ) { newBlock = editor.updateBlock(currentBlock, block); - // We make sure to reset the cursor position to the new block as calling - // `updateBlock` may move it out. This generally happens when the content - // changes, or the update makes the block multi-column. - editor.setTextCursorPosition(newBlock); + // Inline conversions preserve content, so keep updateBlock's mapped caret. + // Other updates may move it out (e.g. replacing content or creating columns), + // so reset the cursor to the new block in those cases. + if (!convertsExistingText) { + editor.setTextCursorPosition(newBlock); + } } else { newBlock = editor.insertBlocks([block], currentBlock, "after")[0]; editor.setTextCursorPosition(editor.getTextCursorPosition().nextBlock!); From 8ab1ebc91aae1fe9ad5901ae40fc1634baa60061 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 14:10:46 +0100 Subject: [PATCH 4/6] fix(core): reset slash-menu caret only for content-type changes --- .../getDefaultSlashMenuItems.test.ts | 20 +++++++++++++++++++ .../getDefaultSlashMenuItems.ts | 11 ++++++---- .../getMultiColumnSlashMenuItems.test.tsx | 5 +++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts index 09b865c438..d5e9b2dc53 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts @@ -71,6 +71,26 @@ describe("slash-menu block conversion (#2236)", () => { ]); }); + it("enters a table replacing an empty paragraph before other content", () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph" }, + { id: "following", type: "paragraph", content: "Following text" }, + ], + }); + editor.setTextCursorPosition("source", "start"); + + insertOrUpdateBlockForSlashMenu(editor, { + type: "table", + content: { type: "tableContent", rows: [{ cells: ["Cell"] }] }, + }); + + expect(editor.getTextCursorPosition().block).toMatchObject({ + id: "source", + type: "table", + }); + }); + it("still inserts non-inline blocks below existing text", () => { editor = BlockNoteEditor.create({ initialContent: [ diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index c2c4cc6583..86c77e0c9a 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -95,10 +95,13 @@ export function insertOrUpdateBlockForSlashMenu< currentBlock.content.length === 0)) ) { newBlock = editor.updateBlock(currentBlock, block); - // Inline conversions preserve content, so keep updateBlock's mapped caret. - // Other updates may move it out (e.g. replacing content or creating columns), - // so reset the cursor to the new block in those cases. - if (!convertsExistingText) { + // Preserve the mapped caret for same-content-type updates. A structural + // change (e.g. inline text to a table or columns) can map it outside the + // updated block, so explicitly enter the new block in that case. + if ( + editor.schema.blockSchema[currentBlock.type].content !== + editor.schema.blockSchema[newBlock.type].content + ) { editor.setTextCursorPosition(newBlock); } } else { diff --git a/packages/xl-multi-column/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.test.tsx b/packages/xl-multi-column/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.test.tsx index 0ab7e8c047..2a39d8e413 100644 --- a/packages/xl-multi-column/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.test.tsx +++ b/packages/xl-multi-column/src/extensions/SuggestionMenu/getMultiColumnSlashMenuItems.test.tsx @@ -55,6 +55,11 @@ describe("Test getMultiColumnSlashMenuItems", () => { // The empty paragraph becomes the column list, and keeps its id. expect(getEditor().getBlock("empty-paragraph")?.type).toBe("columnList"); + const cursorBlock = getEditor().getTextCursorPosition().block; + const column = getEditor().getParentBlock(cursorBlock); + expect(cursorBlock.type).toBe("paragraph"); + expect(column?.type).toBe("column"); + expect(getEditor().getParentBlock(column!)?.id).toBe("empty-paragraph"); expect(getEditor().document).toMatchSnapshot(); }); }); From 686181de15a8ed012b2bf8f2670a6a20f78fee85 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 14:33:38 +0100 Subject: [PATCH 5/6] fix(core): limit slash-menu text conversion to block start --- .../getDefaultSlashMenuItems.test.ts | 38 +++++++++++++++---- .../getDefaultSlashMenuItems.ts | 10 +++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts index d5e9b2dc53..f52cb1bee3 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.test.ts @@ -12,12 +12,33 @@ afterEach(() => { }); describe("slash-menu block conversion (#2236)", () => { + it("converts the existing paragraph to a quote at its start", () => { + editor = BlockNoteEditor.create({ + initialContent: [ + { id: "source", type: "paragraph", content: "Existing text" }, + ], + }); + // The suggestion menu removes its slash/query before invoking the item. + editor.setTextCursorPosition("source", "start"); + + insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); + + expect(editor.document).toHaveLength(1); + expect(editor.document[0]).toMatchObject({ + id: "source", + type: "quote", + content: [{ type: "text", text: "Existing text", styles: {} }], + }); + expect(editor.getTextCursorPosition().block.id).toBe("source"); + expect(editor.prosemirrorState.selection.empty).toBe(true); + expect(editor.prosemirrorState.selection.$from.parentOffset).toBe(0); + }); + it.each([ - ["start", 0], ["middle", 8], ["end", 13], ] as const)( - "converts the existing paragraph to a quote at its %s", + "inserts a new quote below when invoked at the paragraph's %s", (_position, offset) => { editor = BlockNoteEditor.create({ initialContent: [ @@ -34,15 +55,18 @@ describe("slash-menu block conversion (#2236)", () => { insertOrUpdateBlockForSlashMenu(editor, { type: "quote" }); - expect(editor.document).toHaveLength(1); + expect(editor.document).toHaveLength(2); expect(editor.document[0]).toMatchObject({ id: "source", - type: "quote", + type: "paragraph", content: [{ type: "text", text: "Existing text", styles: {} }], }); - expect(editor.getTextCursorPosition().block.id).toBe("source"); + expect(editor.document[1]).toMatchObject({ type: "quote", content: [] }); + expect(editor.getTextCursorPosition().block.id).toBe( + editor.document[1].id, + ); expect(editor.prosemirrorState.selection.empty).toBe(true); - expect(editor.prosemirrorState.selection.$from.parentOffset).toBe(offset); + expect(editor.prosemirrorState.selection.$from.parentOffset).toBe(0); }, ); @@ -52,7 +76,7 @@ describe("slash-menu block conversion (#2236)", () => { { id: "source", type: "paragraph", content: "Existing text" }, ], }); - editor.setTextCursorPosition("source", "end"); + editor.setTextCursorPosition("source", "start"); insertOrUpdateBlockForSlashMenu(editor, { type: "quote", diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 86c77e0c9a..e788b4fde3 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -54,9 +54,9 @@ function setSelectionToNextContentEditableBlock< } // Updates an empty/slash-only block, or converts an inline-content block when -// choosing another inline type at any caret position, instead of inserting below. If the new -// block doesn't contain editable content, the cursor is moved to the next block -// that does. +// choosing another inline type at the start of the block, instead of inserting +// below. If the new block doesn't contain editable content, the cursor is moved +// to the next block that does. export function insertOrUpdateBlockForSlashMenu< BSchema extends BlockSchema, I extends InlineContentSchema, @@ -75,11 +75,13 @@ export function insertOrUpdateBlockForSlashMenu< const { selection } = editor.prosemirrorState; // The menu has already removed the slash/query. With a collapsed cursor, - // choosing an inline block type converts the current block at any position. + // choosing an inline block type at the start converts the current block. + // At other positions, insert a new block below without changing existing text. // Only convert between inline-content types when no replacement content was // supplied, so the existing text is preserved; other items insert below. const convertsExistingText = selection.empty && + selection.$from.parentOffset === 0 && editor.schema.blockSchema[currentBlock.type].content === "inline" && block.type !== undefined && editor.schema.blockSchema[block.type].content === "inline" && From 488625e356fe4682b4f787c9a671a96b65b20190 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 30 Oct 2026 15:10:54 +0100 Subject: [PATCH 6/6] test: wait for nesting suggestion decorations before screenshot --- tests/src/end-to-end/y-prosemirror/nesting.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/end-to-end/y-prosemirror/nesting.test.tsx b/tests/src/end-to-end/y-prosemirror/nesting.test.tsx index 620f926321..76aafb6ac9 100644 --- a/tests/src/end-to-end/y-prosemirror/nesting.test.tsx +++ b/tests/src/end-to-end/y-prosemirror/nesting.test.tsx @@ -97,6 +97,10 @@ test("suggestion mode: change block type of a block with children", async () => // TODO: should this be editor.document[0], or expose .documentWithoutDeletions? await expect.poll(() => editor.document[1]?.type).toBe("heading"); + // The type updates before the asynchronous suggestion decorations settle. + // Wait for the final attributed structure before capturing its styling. + await waitForSuggestion(editor); + await expectScreenshot( screen.getByTestId("editor-root"), "nesting-change-parent-type",