From 699e333e1f780af2004b3980a575b7ff6008e675 Mon Sep 17 00:00:00 2001 From: ihordubas99 Date: Thu, 23 Apr 2026 17:06:03 +0300 Subject: [PATCH 1/3] fix: preserve text after cursor when inserting `#` trigger --- lib/components/HashMenu/HashWpMenu.tsx | 50 ++++++++++----- lib/components/HashMenu/editorUtils.ts | 86 ++++++++------------------ 2 files changed, 59 insertions(+), 77 deletions(-) diff --git a/lib/components/HashMenu/HashWpMenu.tsx b/lib/components/HashMenu/HashWpMenu.tsx index 8c4f356..3b037fb 100644 --- a/lib/components/HashMenu/HashWpMenu.tsx +++ b/lib/components/HashMenu/HashWpMenu.tsx @@ -1,4 +1,5 @@ import type { FC, RefObject } from "react"; +import { useRef } from "react"; import type { SuggestionMenuProps } from "@blocknote/react"; import styled from "styled-components"; import { BlockCard } from "../BlockWorkPackage/BlockCard"; @@ -6,11 +7,10 @@ import { defaultWpVariables } from "../WorkPackage/atoms"; import type { WorkPackage } from "../../openProjectTypes"; import type { HashMenuItem } from "./types"; import type { AnyEditor } from "./editorUtils"; +import type { InlineWpSize } from "../WorkPackage/types"; import { getSizeFromCurrentBlock, - clearTriggerText, insertWpChip, - insertWpChipIntoBlock, } from "./editorUtils"; const Menu = styled.div.attrs({ className: "op-bn-hash-menu" })` @@ -54,27 +54,48 @@ export function createHashWpMenuComponent( const searchQuery = items[0]?.title ?? ""; const visibleResults = (resultsRef.current ?? []).slice(0, MAX_RESULTS); + const pendingSizeRef = useRef("xxs"); + const originalBlockIdRef = useRef(undefined); + const savedSelectionRef = useRef(null); + + const currentSize = getSizeFromCurrentBlock(editor); + const currentBlockId = editor.getTextCursorPosition()?.block?.id; + // Mutate each item's onItemClick so BlockNote's keyboard handler // (Enter / PgUp / PgDn) calls the correct insertion for that result. visibleResults.forEach((wp, index) => { if (!items[index]) return; - const size = getSizeFromCurrentBlock(editor); - const blockId = editor.getTextCursorPosition()?.block?.id; - items[index].onItemClick = () => { + const size = pendingSizeRef.current !== "xxs" + ? pendingSizeRef.current + : currentSize; + const originalBlockId = originalBlockIdRef.current ?? currentBlockId; + const savedSelection = savedSelectionRef.current; + + pendingSizeRef.current = "xxs"; + originalBlockIdRef.current = undefined; + savedSelectionRef.current = null; + requestAnimationFrame(() => { - if (!blockId) return; + if (!originalBlockId) return; editor.focus(); // BlockNote splits the block on Enter - remove the new empty block it created. const currentBlock = editor.getTextCursorPosition()?.block; - if (currentBlock && currentBlock.id !== blockId) { + if (currentBlock && currentBlock.id !== originalBlockId) { editor.removeBlocks([currentBlock.id]); } - clearTriggerText(editor); - insertWpChipIntoBlock(editor, blockId, wp, size); + if (savedSelection) { + const tiptap = (editor as any)._tiptapEditor; + if (tiptap) { + const tr = tiptap.state.tr.setSelection(savedSelection); + tiptap.view.dispatch(tr); + } + } + + insertWpChip(editor, wp, size); }); }; }); @@ -105,13 +126,10 @@ export function createHashWpMenuComponent( // cleanup, so we clear the trigger text manually before inserting. onMouseDown={(e) => { e.preventDefault(); - const size = getSizeFromCurrentBlock(editor); - const blockId = clearTriggerText(editor); - if (blockId) { - editor.focus(); - editor.setTextCursorPosition(blockId, "end"); - } - insertWpChip(editor, wp, size); + pendingSizeRef.current = getSizeFromCurrentBlock(editor); + originalBlockIdRef.current = editor.getTextCursorPosition()?.block?.id; + savedSelectionRef.current = (editor as any)._tiptapEditor?.state?.selection ?? null; + items[index]?.onItemClick(); }} > diff --git a/lib/components/HashMenu/editorUtils.ts b/lib/components/HashMenu/editorUtils.ts index 90cb331..fbca261 100644 --- a/lib/components/HashMenu/editorUtils.ts +++ b/lib/components/HashMenu/editorUtils.ts @@ -22,7 +22,7 @@ export function getSizeFromCurrentBlock(editor: AnyEditor): InlineWpSize { for (const node of content) { if (node.type !== "text") continue; const text = node.text as string; - const match = text.match(/(#+)[^#]/); + const match = text.match(/(#+)/); if (match) { const hashCount = match[1].length; if (hashCount >= 3) return "s"; @@ -34,43 +34,30 @@ export function getSizeFromCurrentBlock(editor: AnyEditor): InlineWpSize { return "xxs"; } -/** - * Removes the # trigger text (and any extra # symbols) from the current block. - * BlockNote removes #query itself on Enter, but may leave extra # characters - * (e.g. ## from ###query). Returns the block id, or null if nothing was found. - */ export function clearTriggerText(editor: AnyEditor): string | null { - const block = editor.getTextCursorPosition()?.block; - if (!block) return null; - - const content = (block.content ?? []) as any[]; - - const triggerNodeIndex = content.findIndex((n) => { - if (n.type !== "text") return false; - return /#+/.test(n.text as string); - }); + const tiptap = (editor as any)._tiptapEditor; + if (!tiptap) return null; - if (triggerNodeIndex === -1) return null; + const { state, view } = tiptap; + const { selection } = state; + const { $from, from } = selection; - const triggerNode = content[triggerNodeIndex] as { type: string; text: string; styles: any }; - const text = triggerNode.text; - const hashIndex = text.search(/#/); - const textBefore = hashIndex > 0 ? text.slice(0, hashIndex) : null; + const textBefore = $from.parent.textBetween( + Math.max(0, $from.parentOffset - 50), + $from.parentOffset, + undefined, + "\n" + ); - const cleanedContent = [ - ...content.slice(0, triggerNodeIndex), - ...(textBefore ? [{ type: "text", text: textBefore, styles: triggerNode.styles }] : []), - ]; + const match = textBefore.match(/(#+\S*)$/); + if (!match) return null; - editor.updateBlock(block.id, { content: cleanedContent } as any); - return block.id; -} + const triggerLength = match[1].length; + const tr = state.tr.delete(from - triggerLength, from); + view.dispatch(tr); -function focusAndMoveToEnd(editor: AnyEditor, blockId: string): void { - requestAnimationFrame(() => { - editor.focus(); - editor.setTextCursorPosition(blockId, "end"); - }); + const block = (editor as any).getTextCursorPosition()?.block; + return block?.id ?? null; } /** @@ -80,43 +67,20 @@ function focusAndMoveToEnd(editor: AnyEditor, blockId: string): void { export function insertWpChip(editor: AnyEditor, wp: WorkPackage, size: InlineWpSize): void { const instanceId = makeInstanceId(); + clearTriggerText(editor); + (editor.insertInlineContent as (content: unknown[]) => void)([ { type: "openProjectWorkPackageInline", props: { wpid: String(wp.id), instanceId, size } }, { type: "text", text: " ", styles: {} }, ]); - requestAnimationFrame(() => { - editor.focus(); - const cursor = editor.getTextCursorPosition(); - if (cursor?.block?.id) { - editor.setTextCursorPosition(cursor.block.id, "end"); - } - }); + editor.focus(); } -/** - * Keyboard (Enter) path: inserts chip directly into block content by ID, - * bypassing cursor position entirely to avoid race conditions with - * BlockNote's Enter handling which moves the cursor to a new block. - */ export function insertWpChipIntoBlock( editor: AnyEditor, - blockId: string, + _blockId: string, wp: WorkPackage, - size: InlineWpSize, + size: InlineWpSize ): void { - const instanceId = makeInstanceId(); - const block = editor.getBlock(blockId); - if (!block) return; - - const content = (block.content ?? []) as any[]; - - editor.updateBlock(blockId, { - content: [ - ...content, - { type: "openProjectWorkPackageInline", props: { wpid: String(wp.id), instanceId, size } }, - { type: "text", text: " ", styles: {} }, - ], - } as any); - - focusAndMoveToEnd(editor, blockId); + insertWpChip(editor, wp, size); } \ No newline at end of file From c047379ec0d3532e3fe0efde46d47a5455dc9f97 Mon Sep 17 00:00:00 2001 From: ihordubas99 Date: Thu, 23 Apr 2026 17:19:07 +0300 Subject: [PATCH 2/3] test: adapt unit tests to TipTap-based clearTriggerText --- test/lib/components/hashMenu.test.ts | 83 +++++++++++++++++-- .../editor.inlineChip.browser.test.tsx | 14 ++-- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/test/lib/components/hashMenu.test.ts b/test/lib/components/hashMenu.test.ts index cadf90f..ec14bca 100644 --- a/test/lib/components/hashMenu.test.ts +++ b/test/lib/components/hashMenu.test.ts @@ -8,20 +8,77 @@ import { type FakeContent = { type: string; text?: string; styles?: any; props?: any }[]; +function findCursorPos(fullText: string): number { + const match = fullText.match(/#+/); + if (!match || match.index === undefined) return fullText.length; + return match.index + match[0].length; +} + +function makeFakeTiptap(block: { id: string; content: FakeContent }) { + const getFullText = () => + block.content + .filter((n) => n.type === "text") + .map((n) => n.text ?? "") + .join(""); + + return { + get state() { + const fullText = getFullText(); + const cursorPos = findCursorPos(fullText); + + return { + selection: { + from: cursorPos, + $from: { + parentOffset: cursorPos, + parent: { + textBetween(start: number, end: number) { + const text = getFullText(); + const absStart = Math.max(0, cursorPos - (end - start)); + return text.slice(absStart, cursorPos); + }, + }, + }, + }, + tr: { + delete(start: number, end: number) { + return { _start: start, _end: end }; + }, + }, + }; + }, + view: { + dispatch(tr: { _start: number; _end: number }) { + const fullText = getFullText(); + const hashIndex = fullText.search(/#+/); + const newText = hashIndex <= 0 ? "" : fullText.slice(0, hashIndex); + + if (newText === "") { + block.content = []; + } else { + const firstTextNode = block.content.find((n) => n.type === "text"); + const nonTextNodes = block.content.filter((n) => n.type !== "text"); + block.content = [ + ...(firstTextNode ? [{ ...firstTextNode, text: newText }] : []), + ...nonTextNodes, + ]; + } + }, + }, + }; +} + function makeFakeEditor(content: FakeContent = []) { - let block = { id: "block-1", content }; + const block = { id: "block-1", content: [...content] }; const inserted: FakeContent = []; - return { + const editor: any = { block, inserted, getTextCursorPosition: () => ({ block }), getBlock: (id: string) => (id === block.id ? block : null), updateBlock: (id: string, update: { content: FakeContent }) => { - if (id === block.id) { - block.content = update.content; - inserted.push(...update.content); - } + if (id === block.id) block.content = update.content; }, insertInlineContent: (content: FakeContent) => { inserted.push(...content); @@ -29,6 +86,10 @@ function makeFakeEditor(content: FakeContent = []) { focus: vi.fn(), setTextCursorPosition: vi.fn(), }; + + editor._tiptapEditor = makeFakeTiptap(block); + + return editor; } describe("getSizeFromCurrentBlock", () => { @@ -65,7 +126,11 @@ describe("clearTriggerText", () => { }); it("does nothing if no block", () => { - const editor = { getTextCursorPosition: () => null, updateBlock: vi.fn() }; + const editor = { + _tiptapEditor: null, + getTextCursorPosition: () => null, + updateBlock: vi.fn(), + }; expect(clearTriggerText(editor as any)).toBeNull(); }); @@ -98,8 +163,8 @@ describe("insertWpChipIntoBlock", () => { expect(inserted.length).toBe(2); const chip = inserted.find( - (c): c is { type: string; props: { size: string } } => - c.type === "openProjectWorkPackageInline" && c.props?.size + (item: any): item is { type: string; props: { size: string } } => + item.type === "openProjectWorkPackageInline" && item.props?.size ); expect(chip).toBeDefined(); expect(chip?.props.size).toBe("xxs"); diff --git a/test/lib/components/integration/editor.inlineChip.browser.test.tsx b/test/lib/components/integration/editor.inlineChip.browser.test.tsx index fbab81f..1465694 100644 --- a/test/lib/components/integration/editor.inlineChip.browser.test.tsx +++ b/test/lib/components/integration/editor.inlineChip.browser.test.tsx @@ -13,7 +13,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaSlashMenu(); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).toBeVisible(); await expect.element(page.getByText('Fix login bug')).toBeVisible(); @@ -23,7 +23,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('#'); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); }); @@ -32,7 +32,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('##'); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); }); @@ -41,7 +41,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('###'); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).toBeVisible(); }); @@ -55,7 +55,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Tiny (inline)', exact: true })); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).not.toBeInTheDocument(); @@ -68,7 +68,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Compact (inline)', exact: true })); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).toBeVisible(); @@ -81,7 +81,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Tiny (inline)', exact: true })); - await expect.element(page.getByText('#123')).toBeVisible(); + await expect.element(page.getByText('#123').first()).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).not.toBeInTheDocument(); From 0dd1e54336fe4c2fc2c9b1e5c8d01799f563ce5e Mon Sep 17 00:00:00 2001 From: ihordubas99 Date: Mon, 27 Apr 2026 14:22:41 +0300 Subject: [PATCH 3/3] refactor(editor): simplify hash menu state and use real editor in tests --- lib/components/HashMenu/HashWpMenu.tsx | 44 ++--- test/lib/components/hashMenu.test.ts | 158 +++++------------- .../editor.inlineChip.browser.test.tsx | 14 +- 3 files changed, 61 insertions(+), 155 deletions(-) diff --git a/lib/components/HashMenu/HashWpMenu.tsx b/lib/components/HashMenu/HashWpMenu.tsx index 3b037fb..1c29eae 100644 --- a/lib/components/HashMenu/HashWpMenu.tsx +++ b/lib/components/HashMenu/HashWpMenu.tsx @@ -1,5 +1,4 @@ import type { FC, RefObject } from "react"; -import { useRef } from "react"; import type { SuggestionMenuProps } from "@blocknote/react"; import styled from "styled-components"; import { BlockCard } from "../BlockWorkPackage/BlockCard"; @@ -7,7 +6,6 @@ import { defaultWpVariables } from "../WorkPackage/atoms"; import type { WorkPackage } from "../../openProjectTypes"; import type { HashMenuItem } from "./types"; import type { AnyEditor } from "./editorUtils"; -import type { InlineWpSize } from "../WorkPackage/types"; import { getSizeFromCurrentBlock, insertWpChip, @@ -54,10 +52,6 @@ export function createHashWpMenuComponent( const searchQuery = items[0]?.title ?? ""; const visibleResults = (resultsRef.current ?? []).slice(0, MAX_RESULTS); - const pendingSizeRef = useRef("xxs"); - const originalBlockIdRef = useRef(undefined); - const savedSelectionRef = useRef(null); - const currentSize = getSizeFromCurrentBlock(editor); const currentBlockId = editor.getTextCursorPosition()?.block?.id; @@ -67,35 +61,22 @@ export function createHashWpMenuComponent( if (!items[index]) return; items[index].onItemClick = () => { - const size = pendingSizeRef.current !== "xxs" - ? pendingSizeRef.current - : currentSize; - const originalBlockId = originalBlockIdRef.current ?? currentBlockId; - const savedSelection = savedSelectionRef.current; - - pendingSizeRef.current = "xxs"; - originalBlockIdRef.current = undefined; - savedSelectionRef.current = null; - requestAnimationFrame(() => { - if (!originalBlockId) return; - editor.focus(); + if (!currentBlockId) return; - // BlockNote splits the block on Enter - remove the new empty block it created. const currentBlock = editor.getTextCursorPosition()?.block; - if (currentBlock && currentBlock.id !== originalBlockId) { + + // BlockNote's default "Enter" behavior splits the block. + // If the block ID changed, it was a keyboard Enter, so we remove the new empty block. + const isKeyboardEnter = currentBlock && currentBlock.id !== currentBlockId; + + if (isKeyboardEnter) { editor.removeBlocks([currentBlock.id]); } - if (savedSelection) { - const tiptap = (editor as any)._tiptapEditor; - if (tiptap) { - const tr = tiptap.state.tr.setSelection(savedSelection); - tiptap.view.dispatch(tr); - } - } + editor.focus(); - insertWpChip(editor, wp, size); + insertWpChip(editor, wp, currentSize); }); }; }); @@ -122,13 +103,10 @@ export function createHashWpMenuComponent( { e.preventDefault(); - pendingSizeRef.current = getSizeFromCurrentBlock(editor); - originalBlockIdRef.current = editor.getTextCursorPosition()?.block?.id; - savedSelectionRef.current = (editor as any)._tiptapEditor?.state?.selection ?? null; items[index]?.onItemClick(); }} > diff --git a/test/lib/components/hashMenu.test.ts b/test/lib/components/hashMenu.test.ts index ec14bca..435f26a 100644 --- a/test/lib/components/hashMenu.test.ts +++ b/test/lib/components/hashMenu.test.ts @@ -1,128 +1,56 @@ // @vitest-environment jsdom import { describe, it, expect, vi } from "vitest"; +import { BlockNoteEditor } from "@blocknote/core"; import { getSizeFromCurrentBlock, insertWpChipIntoBlock, clearTriggerText, } from "../../../lib/components/HashMenu/editorUtils"; -type FakeContent = { type: string; text?: string; styles?: any; props?: any }[]; - -function findCursorPos(fullText: string): number { - const match = fullText.match(/#+/); - if (!match || match.index === undefined) return fullText.length; - return match.index + match[0].length; -} - -function makeFakeTiptap(block: { id: string; content: FakeContent }) { - const getFullText = () => - block.content - .filter((n) => n.type === "text") - .map((n) => n.text ?? "") - .join(""); - - return { - get state() { - const fullText = getFullText(); - const cursorPos = findCursorPos(fullText); - - return { - selection: { - from: cursorPos, - $from: { - parentOffset: cursorPos, - parent: { - textBetween(start: number, end: number) { - const text = getFullText(); - const absStart = Math.max(0, cursorPos - (end - start)); - return text.slice(absStart, cursorPos); - }, - }, - }, - }, - tr: { - delete(start: number, end: number) { - return { _start: start, _end: end }; - }, - }, - }; - }, - view: { - dispatch(tr: { _start: number; _end: number }) { - const fullText = getFullText(); - const hashIndex = fullText.search(/#+/); - const newText = hashIndex <= 0 ? "" : fullText.slice(0, hashIndex); - - if (newText === "") { - block.content = []; - } else { - const firstTextNode = block.content.find((n) => n.type === "text"); - const nonTextNodes = block.content.filter((n) => n.type !== "text"); - block.content = [ - ...(firstTextNode ? [{ ...firstTextNode, text: newText }] : []), - ...nonTextNodes, - ]; - } - }, - }, - }; -} - -function makeFakeEditor(content: FakeContent = []) { - const block = { id: "block-1", content: [...content] }; - const inserted: FakeContent = []; - - const editor: any = { - block, - inserted, - getTextCursorPosition: () => ({ block }), - getBlock: (id: string) => (id === block.id ? block : null), - updateBlock: (id: string, update: { content: FakeContent }) => { - if (id === block.id) block.content = update.content; - }, - insertInlineContent: (content: FakeContent) => { - inserted.push(...content); - }, - focus: vi.fn(), - setTextCursorPosition: vi.fn(), - }; - - editor._tiptapEditor = makeFakeTiptap(block); - +function createTestEditor(text: string) { + const editor = BlockNoteEditor.create({ + initialContent: [{ type: "paragraph", content: text }], + }); + + const block = editor.document[0]; + editor.setTextCursorPosition(block, "end"); + return editor; } describe("getSizeFromCurrentBlock", () => { it("returns xxs for #", () => { - const editor = makeFakeEditor([{ type: "text", text: "#foo" }]); + const editor = createTestEditor("#foo"); expect(getSizeFromCurrentBlock(editor as any)).toBe("xxs"); }); it("returns xs for ##", () => { - const editor = makeFakeEditor([{ type: "text", text: "##foo" }]); + const editor = createTestEditor("##foo"); expect(getSizeFromCurrentBlock(editor as any)).toBe("xs"); }); it("returns s for ### or more", () => { - const editor = makeFakeEditor([{ type: "text", text: "###foo" }]); + const editor = createTestEditor("###foo"); expect(getSizeFromCurrentBlock(editor as any)).toBe("s"); - const editor2 = makeFakeEditor([{ type: "text", text: "####foo" }]); + const editor2 = createTestEditor("####foo"); expect(getSizeFromCurrentBlock(editor2 as any)).toBe("s"); }); it("returns xxs if no hashes", () => { - const editor = makeFakeEditor([{ type: "text", text: "foo" }]); + const editor = createTestEditor("foo"); expect(getSizeFromCurrentBlock(editor as any)).toBe("xxs"); }); }); describe("clearTriggerText", () => { it("removes # text and returns block id", () => { - const editor = makeFakeEditor([{ type: "text", text: "#foo" }]); + const editor = createTestEditor("#foo"); const blockId = clearTriggerText(editor as any); - expect(blockId).toBe("block-1"); - expect(editor.block.content).toEqual([]); + + expect(blockId).toBe(editor.document[0].id); + const block = editor.getBlock(editor.document[0].id); + expect(block?.content).toEqual([]); }); it("does nothing if no block", () => { @@ -135,45 +63,45 @@ describe("clearTriggerText", () => { }); it("keeps text before # and removes trigger", () => { - const editor = makeFakeEditor([{ type: "text", text: "Hello #foo" }]); + const editor = createTestEditor("Hello #foo"); const blockId = clearTriggerText(editor as any); - expect(blockId).toBe("block-1"); - expect(editor.block.content).toEqual([{ type: "text", text: "Hello " }]); + + expect(blockId).toBe(editor.document[0].id); + const block = editor.getBlock(editor.document[0].id); + expect((block?.content as any)[0].text).toBe("Hello "); }); it("works with multiple # in the text", () => { - const editor = makeFakeEditor([{ type: "text", text: "Pre #one #two #three" }]); + const editor = createTestEditor("Pre #one #two #three"); const blockId = clearTriggerText(editor as any); - expect(blockId).toBe("block-1"); - expect(editor.block.content).toEqual([{ type: "text", text: "Pre " }]); + + expect(blockId).toBe(editor.document[0].id); + const block = editor.getBlock(editor.document[0].id); + + expect((block?.content as any)[0].text).toBe("Pre #one #two "); }); }); describe("insertWpChipIntoBlock", () => { it("adds a chip to the block content", () => { - const editor = makeFakeEditor([]); + const editor = createTestEditor("test"); + + const insertSpy = vi.spyOn(editor, "insertInlineContent").mockImplementation(() => {}); + vi.spyOn(editor, "focus").mockImplementation(() => {}); + insertWpChipIntoBlock( editor as any, - "block-1", + editor.document[0].id, { id: 1, subject: "Fix bug" } as any, "xxs" ); - const inserted = editor.inserted; - expect(inserted.length).toBe(2); - - const chip = inserted.find( - (item: any): item is { type: string; props: { size: string } } => - item.type === "openProjectWorkPackageInline" && item.props?.size - ); - expect(chip).toBeDefined(); - expect(chip?.props.size).toBe("xxs"); - }); - - it("does nothing if block not found", () => { - const editor = makeFakeEditor([]); - expect(() => - insertWpChipIntoBlock(editor as any, "wrong-id", { id: 1 } as any, "xxs") - ).not.toThrow(); + expect(insertSpy).toHaveBeenCalledWith([ + { + type: "openProjectWorkPackageInline", + props: { wpid: "1", instanceId: expect.any(String), size: "xxs" }, + }, + { type: "text", text: " ", styles: {} }, + ]); }); }); \ No newline at end of file diff --git a/test/lib/components/integration/editor.inlineChip.browser.test.tsx b/test/lib/components/integration/editor.inlineChip.browser.test.tsx index 1465694..fbab81f 100644 --- a/test/lib/components/integration/editor.inlineChip.browser.test.tsx +++ b/test/lib/components/integration/editor.inlineChip.browser.test.tsx @@ -13,7 +13,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaSlashMenu(); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).toBeVisible(); await expect.element(page.getByText('Fix login bug')).toBeVisible(); @@ -23,7 +23,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('#'); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); }); @@ -32,7 +32,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('##'); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); }); @@ -41,7 +41,7 @@ describe('Inline chip - insert', () => { renderEditor(); await insertInlineChipViaHash('###'); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).toBeVisible(); }); @@ -55,7 +55,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Tiny (inline)', exact: true })); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).not.toBeInTheDocument(); @@ -68,7 +68,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Compact (inline)', exact: true })); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).toBeVisible(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).toBeVisible(); @@ -81,7 +81,7 @@ describe('Inline chip - resize', () => { await openInlineChipSizeMenu(); await userEvent.click(page.getByRole('button', { name: 'Tiny (inline)', exact: true })); - await expect.element(page.getByText('#123').first()).toBeVisible(); + await expect.element(page.getByText('#123')).toBeVisible(); await expect.element(page.getByTestId('op-bn-work-package--type')).not.toBeInTheDocument(); await expect.element(page.getByText('In Progress')).not.toBeInTheDocument(); await expect.element(page.getByText('Fix login bug')).not.toBeInTheDocument();