From e61044974c6afb411e811076eb36a226ee164f36 Mon Sep 17 00:00:00 2001 From: Mikhail Lukin Date: Wed, 5 Aug 2026 18:44:49 +0700 Subject: [PATCH] fix xl-multi-column --- .../src/extensions/DropCursor/dropHandlers.ts | 94 ++++ .../DropCursor/multiColumnHandleDropPlugin.ts | 76 +--- .../util/computeColumnListChildren.test.ts | 161 +++++++ .../util/computeColumnListChildren.ts | 56 +++ .../__snapshots__/dropHandlers.test.ts.snap | 427 ++++++++++++++++++ .../src/test/dropCursor/dropHandlers.test.ts | 178 ++++++++ 6 files changed, 928 insertions(+), 64 deletions(-) create mode 100644 packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts create mode 100644 packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts create mode 100644 packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts create mode 100644 packages/xl-multi-column/src/test/dropCursor/__snapshots__/dropHandlers.test.ts.snap create mode 100644 packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts diff --git a/packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts b/packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts new file mode 100644 index 0000000000..37a451e7f2 --- /dev/null +++ b/packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts @@ -0,0 +1,94 @@ +import type { Block, BlockNoteEditor } from "@blocknote/core"; +import { + getNodeById, + removeAndInsertBlocks, + updateBlock as updateBlockLowLevel, +} from "@blocknote/core"; + +import { computeColumnListChildrenAfterDrop } from "./util/computeColumnListChildren.js"; + +/** + * Handles dropping a block (or column) onto the left/right edge of an + * existing column, inserting a new column next to it. + * + * The removal of the dragged item and the update of `columnList`'s children + * happen in a single transaction, with column/columnList collapsing + * ("fixColumns") disabled for the removal step. `newChildren` is computed + * up-front and already accounts for any column left empty by the removal, so + * letting `fixColumnList` also run on the removal step would collapse (and + * invalidate the id of) the very `columnList` we're about to update. + */ +export function dropOntoColumn( + editor: BlockNoteEditor, + params: { + columnList: Block; + targetColumnId: string; + draggedBlock: Block; + position: "left" | "right"; + }, +) { + const { columnList, targetColumnId, draggedBlock, position } = params; + + if (targetColumnId === draggedBlock.id) { + return; + } + + const newChildren = computeColumnListChildrenAfterDrop( + columnList, + draggedBlock, + targetColumnId, + position, + ); + + editor.transact((tr) => { + if (getNodeById(draggedBlock.id, tr.doc)) { + removeAndInsertBlocks(tr, [draggedBlock.id], [], { fixColumns: false }); + } + + updateBlockLowLevel(tr, columnList.id, { children: newChildren }); + }); +} + +/** + * Handles dropping a block onto the left/right edge of a block that isn't + * inside a column, wrapping both blocks in a new `columnList`. + */ +export function dropOntoBlock( + editor: BlockNoteEditor, + params: { + targetBlock: Block; + draggedBlock: Block; + position: "left" | "right"; + }, +) { + const { targetBlock, draggedBlock, position } = params; + + if (targetBlock.id === draggedBlock.id) { + return; + } + + const blocks = + position === "left" + ? [draggedBlock, targetBlock] + : [targetBlock, draggedBlock]; + + editor.transact((tr) => { + if (getNodeById(draggedBlock.id, tr.doc)) { + removeAndInsertBlocks(tr, [draggedBlock.id], []); + } + + removeAndInsertBlocks( + tr, + [targetBlock.id], + [ + { + type: "columnList", + children: blocks.map((block) => ({ + type: "column" as const, + children: [block], + })), + }, + ], + ); + }); +} diff --git a/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts b/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts index 7c8e0b312e..2f1d0dc2ce 100644 --- a/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts +++ b/packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts @@ -1,12 +1,8 @@ import type { BlockNoteEditor } from "@blocknote/core"; -import { - UniqueID, - createExtension, - getBlockInfo, - nodeToBlock, -} from "@blocknote/core"; +import { createExtension, getBlockInfo, nodeToBlock } from "@blocknote/core"; import { Plugin } from "prosemirror-state"; import type { EditorView } from "prosemirror-view"; +import { dropOntoBlock, dropOntoColumn } from "./dropHandlers.js"; import { detectEdgePosition } from "./multiColumnDropCursor.js"; /** @@ -79,69 +75,21 @@ export function createMultiColumnHandleDropPlugin( }); } - const index = columnList.children.findIndex( - (b) => b.id === blockInfo.bnBlock.node.attrs.id, - ); - - const newChildren = columnList.children - // If the dragged block is in one of the columns, remove it. - .map((column) => ({ - ...column, - children: column.children.filter( - (block) => block.id !== draggedBlock.id, - ), - })) - // Remove empty columns (can happen when dragged block is removed). - .filter((column) => column.children.length > 0) - // Insert the dragged block in the correct position. - .toSpliced(edgePos.position === "left" ? index : index + 1, 0, { - type: "column", - children: [draggedBlock], - props: {}, - content: undefined, - id: UniqueID.options.generateID(), - }); - - if (editor.getBlock(draggedBlock.id)) { - editor.removeBlocks([draggedBlock]); - } - - editor.updateBlock(columnList, { - children: newChildren, + dropOntoColumn(editor, { + columnList, + targetColumnId: blockInfo.bnBlock.node.attrs.id, + draggedBlock, + position: edgePos.position, }); } else { // Create new columnList with blocks as columns const block = nodeToBlock(blockInfo.bnBlock.node, view.state.doc); - // The user is dropping next to the original block being dragged - do - // nothing. - if (block.id === draggedBlock.id) { - return true; - } - - const blocks = - edgePos.position === "left" - ? [draggedBlock, block] - : [block, draggedBlock]; - - if (editor.getBlock(draggedBlock.id)) { - editor.removeBlocks([draggedBlock]); - } - - editor.replaceBlocks( - [block], - [ - { - type: "columnList", - children: blocks.map((b) => { - return { - type: "column", - children: [b], - }; - }), - }, - ], - ); + dropOntoBlock(editor, { + targetBlock: block, + draggedBlock, + position: edgePos.position, + }); } return true; // Prevent default ProseMirror drop behavior diff --git a/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts b/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts new file mode 100644 index 0000000000..100e46e921 --- /dev/null +++ b/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { computeColumnListChildrenAfterDrop } from "./computeColumnListChildren.js"; + +const paragraph = (id: string, content = "") => ({ + id, + type: "paragraph" as const, + props: {}, + content, + children: [], +}); + +const column = (id: string, children: ReturnType[]) => ({ + id, + type: "column" as const, + props: { width: 1 }, + content: undefined, + children, +}); + +describe("computeColumnListChildrenAfterDrop", () => { + it("keeps the source column when it still has other children", () => { + const columnList = { + id: "column-list", + type: "columnList", + props: {}, + content: undefined, + children: [ + column("column-a", [ + paragraph("dragged", "Dragged"), + paragraph("stays", "Stays"), + ]), + column("column-b", [paragraph("target", "Target")]), + ], + } as any; + + const result = computeColumnListChildrenAfterDrop( + columnList, + columnList.children[0].children[0], + "column-b", + "left", + ); + + expect(result.map((c: any) => c.id)).toEqual([ + "column-a", + expect.any(String), + "column-b", + ]); + expect(result[0].children.map((b: any) => b.id)).toEqual(["stays"]); + expect(result[1].children.map((b: any) => b.id)).toEqual(["dragged"]); + }); + + it("drops the source column entirely once its only block is dragged away (crash trigger)", () => { + const columnList = { + id: "column-list", + type: "columnList", + props: {}, + content: undefined, + children: [ + column("column-a", [paragraph("dragged", "Dragged")]), + column("column-b", [paragraph("target", "Target")]), + ], + } as any; + + const result = computeColumnListChildrenAfterDrop( + columnList, + columnList.children[0].children[0], + "column-b", + "left", + ); + + expect(result.map((c: any) => c.id)).not.toContain("column-a"); + expect(result.map((c: any) => c.id)).toEqual([ + expect.any(String), + "column-b", + ]); + expect(result[0].children.map((b: any) => b.id)).toEqual(["dragged"]); + }); + + it("resolves the target index against the post-filter list, not the original one (left edge)", () => { + const columnList = { + id: "column-list", + type: "columnList", + props: {}, + content: undefined, + children: [ + column("column-a", [paragraph("dragged", "Dragged")]), + column("column-b", [paragraph("b", "B")]), + column("column-c", [paragraph("c", "C")]), + ], + } as any; + + const result = computeColumnListChildrenAfterDrop( + columnList, + columnList.children[0].children[0], + "column-c", + "left", + ); + + expect(result.map((c: any) => c.id)).toEqual([ + "column-b", + expect.any(String), + "column-c", + ]); + }); + + it("resolves the target index against the post-filter list, not the original one (right edge)", () => { + const columnList = { + id: "column-list", + type: "columnList", + props: {}, + content: undefined, + children: [ + column("column-a", [paragraph("dragged", "Dragged")]), + column("column-b", [paragraph("b", "B")]), + column("column-c", [paragraph("c", "C")]), + ], + } as any; + + const result = computeColumnListChildrenAfterDrop( + columnList, + columnList.children[0].children[0], + "column-c", + "right", + ); + + expect(result.map((c: any) => c.id)).toEqual([ + "column-b", + "column-c", + expect.any(String), + ]); + }); + + it("removes the dragged item from the top level when it is itself a column", () => { + const draggedColumn = column("column-a", [paragraph("dragged", "Dragged")]); + const columnList = { + id: "column-list", + type: "columnList", + props: {}, + content: undefined, + children: [ + draggedColumn, + column("column-b", [paragraph("target", "Target")]), + ], + } as any; + + const result = computeColumnListChildrenAfterDrop( + columnList, + draggedColumn as any, + "column-b", + "left", + ); + + expect(result.map((c: any) => c.id)).toEqual([ + expect.any(String), + "column-b", + ]); + expect(result[0].id).not.toBe("column-a"); + expect(result[0].children.map((b: any) => b.id)).toEqual(["dragged"]); + }); +}); diff --git a/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts b/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts new file mode 100644 index 0000000000..e268ca03e1 --- /dev/null +++ b/packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts @@ -0,0 +1,56 @@ +import type { Block } from "@blocknote/core"; +import { UniqueID } from "@blocknote/core"; + +/** + * Computes the new `children` array for a `columnList` after a block (or a + * whole column) is dropped onto the left/right edge of one of its columns. + * + * The dragged item is removed from wherever it currently sits (either as a + * top-level column, or as a block nested inside one of the columns), any + * column left empty by that removal is dropped, and a new column wrapping + * the dragged item is inserted next to the target column. + * + * The index of the target column is resolved against the already-filtered + * list, so removing a column ahead of the target doesn't shift where the new + * column ends up. + */ +export function computeColumnListChildrenAfterDrop( + columnList: Block, + draggedBlock: Block, + targetColumnId: string, + position: "left" | "right", +): Block[] { + const draggedIsColumn = draggedBlock.type === "column"; + + const withDraggedRemoved = columnList.children + .filter((column) => !draggedIsColumn || column.id !== draggedBlock.id) + .map((column) => + draggedIsColumn + ? column + : { + ...column, + children: column.children.filter( + (block) => block.id !== draggedBlock.id, + ), + }, + ) + .filter((column) => column.children.length > 0); + + const targetIndex = withDraggedRemoved.findIndex( + (column) => column.id === targetColumnId, + ); + + const newColumn = { + type: "column" as const, + children: draggedIsColumn ? draggedBlock.children : [draggedBlock], + props: {}, + content: undefined, + id: UniqueID.options.generateID(), + }; + + return withDraggedRemoved.toSpliced( + position === "left" ? targetIndex : targetIndex + 1, + 0, + newColumn, + ); +} diff --git a/packages/xl-multi-column/src/test/dropCursor/__snapshots__/dropHandlers.test.ts.snap b/packages/xl-multi-column/src/test/dropCursor/__snapshots__/dropHandlers.test.ts.snap new file mode 100644 index 0000000000..a7d26b540e --- /dev/null +++ b/packages/xl-multi-column/src/test/dropCursor/__snapshots__/dropHandlers.test.ts.snap @@ -0,0 +1,427 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`dropOntoBlock > wraps the dragged block and target block in a new columnList 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Nested Paragraph 0", + "type": "text", + }, + ], + "id": "nested-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": [ + { + "styles": {}, + "text": "Paragraph 0", + "type": "text", + }, + ], + "id": "paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "1", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 1", + "type": "text", + }, + ], + "id": "paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "2", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "0", + "props": {}, + "type": "columnList", + }, + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 0", + "type": "text", + }, + ], + "id": "column-paragraph-0", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 1", + "type": "text", + }, + ], + "id": "column-paragraph-1", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-0", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 2", + "type": "text", + }, + ], + "id": "column-paragraph-2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Column Paragraph 3", + "type": "text", + }, + ], + "id": "column-paragraph-3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-1", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-0", + "props": {}, + "type": "columnList", + }, +] +`; + +exports[`dropOntoColumn > 2-column layout, source column emptied by the drag (crash regression) > does not throw when dropped on the left edge of the sibling column 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Solo", + "type": "text", + }, + ], + "id": "solo", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "0", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Sibling", + "type": "text", + }, + ], + "id": "sibling", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-sibling", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-solo", + "props": {}, + "type": "columnList", + }, +] +`; + +exports[`dropOntoColumn > 2-column layout, source column emptied by the drag (crash regression) > does not throw when dropped on the right edge of the sibling column 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Sibling", + "type": "text", + }, + ], + "id": "sibling", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-sibling", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Solo", + "type": "text", + }, + ], + "id": "solo", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "0", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-solo", + "props": {}, + "type": "columnList", + }, +] +`; + +exports[`dropOntoColumn > 2-column layout, source column keeps other blocks (baseline) > moves the dragged block without disturbing its sibling 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Stays", + "type": "text", + }, + ], + "id": "stays", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-source", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Dragged", + "type": "text", + }, + ], + "id": "dragged", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "0", + "props": { + "width": 1, + }, + "type": "column", + }, + { + "children": [ + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Target", + "type": "text", + }, + ], + "id": "target", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + ], + "content": undefined, + "id": "column-target", + "props": { + "width": 1, + }, + "type": "column", + }, + ], + "content": undefined, + "id": "column-list-baseline", + "props": {}, + "type": "columnList", + }, +] +`; diff --git a/packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts b/packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts new file mode 100644 index 0000000000..be8d608bf2 --- /dev/null +++ b/packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts @@ -0,0 +1,178 @@ +import { beforeEach, describe, expect, it } from "vite-plus/test"; + +import { + dropOntoBlock, + dropOntoColumn, +} from "../../extensions/DropCursor/dropHandlers.js"; +import { setupTestEnv } from "../setupTestEnv.js"; + +const getEditor = setupTestEnv(); + +describe("dropOntoColumn", () => { + describe("2-column layout, source column emptied by the drag (crash regression)", () => { + beforeEach(() => { + getEditor().replaceBlocks(getEditor().document, [ + { + id: "column-list-solo", + type: "columnList", + children: [ + { + id: "column-solo", + type: "column", + children: [{ id: "solo", type: "paragraph", content: "Solo" }], + }, + { + id: "column-sibling", + type: "column", + children: [ + { id: "sibling", type: "paragraph", content: "Sibling" }, + ], + }, + ], + }, + ]); + }); + + it("does not throw when dropped on the left edge of the sibling column", () => { + const columnList = getEditor().getBlock("column-list-solo")!; + const draggedBlock = getEditor().getBlock("solo")!; + + expect(() => + dropOntoColumn(getEditor(), { + columnList, + targetColumnId: "column-sibling", + draggedBlock, + position: "left", + }), + ).not.toThrow(); + + expect(getEditor().document).toMatchSnapshot(); + }); + + it("does not throw when dropped on the right edge of the sibling column", () => { + const columnList = getEditor().getBlock("column-list-solo")!; + const draggedBlock = getEditor().getBlock("solo")!; + + expect(() => + dropOntoColumn(getEditor(), { + columnList, + targetColumnId: "column-sibling", + draggedBlock, + position: "right", + }), + ).not.toThrow(); + + expect(getEditor().document).toMatchSnapshot(); + }); + }); + + describe("2-column layout, source column keeps other blocks (baseline)", () => { + beforeEach(() => { + getEditor().replaceBlocks(getEditor().document, [ + { + id: "column-list-baseline", + type: "columnList", + children: [ + { + id: "column-source", + type: "column", + children: [ + { id: "dragged", type: "paragraph", content: "Dragged" }, + { id: "stays", type: "paragraph", content: "Stays" }, + ], + }, + { + id: "column-target", + type: "column", + children: [ + { id: "target", type: "paragraph", content: "Target" }, + ], + }, + ], + }, + ]); + }); + + it("moves the dragged block without disturbing its sibling", () => { + const columnList = getEditor().getBlock("column-list-baseline")!; + const draggedBlock = getEditor().getBlock("dragged")!; + + expect(() => + dropOntoColumn(getEditor(), { + columnList, + targetColumnId: "column-target", + draggedBlock, + position: "left", + }), + ).not.toThrow(); + + expect(getEditor().document).toMatchSnapshot(); + }); + }); + + describe("3-column layout, stale splice index", () => { + beforeEach(() => { + getEditor().replaceBlocks(getEditor().document, [ + { + id: "column-list-three", + type: "columnList", + children: [ + { + id: "column-a", + type: "column", + children: [{ id: "a-solo", type: "paragraph", content: "A" }], + }, + { + id: "column-b", + type: "column", + children: [{ id: "b", type: "paragraph", content: "B" }], + }, + { + id: "column-c", + type: "column", + children: [{ id: "c", type: "paragraph", content: "C" }], + }, + ], + }, + ]); + }); + + it("inserts the new column immediately before the target after the source column is dropped", () => { + const columnList = getEditor().getBlock("column-list-three")!; + const draggedBlock = getEditor().getBlock("a-solo")!; + + dropOntoColumn(getEditor(), { + columnList, + targetColumnId: "column-c", + draggedBlock, + position: "left", + }); + + const updated = getEditor().getBlock("column-list-three")!; + expect(updated.children.map((c) => c.id)).toEqual([ + "column-b", + expect.any(String), + "column-c", + ]); + }); + }); +}); + +describe("dropOntoBlock", () => { + it("wraps the dragged block and target block in a new columnList", () => { + getEditor().setTextCursorPosition("paragraph-1"); + + const targetBlock = getEditor().getBlock("paragraph-1")!; + const draggedBlock = getEditor().getBlock("paragraph-2")!; + + expect(() => + dropOntoBlock(getEditor(), { + targetBlock, + draggedBlock, + position: "left", + }), + ).not.toThrow(); + + expect(getEditor().document).toMatchSnapshot(); + }); +});