File
Blob: tests/client/components/editor/lib/page-mention/can-insert.test.ts
| 1 | import { describe, expect, it, vi } from "vitest"; |
| 2 | import type { Editor } from "@tiptap/core"; |
| 3 | import { Schema } from "@tiptap/pm/model"; |
| 4 | import { EditorState, TextSelection } from "@tiptap/pm/state"; |
| 5 | import { schema as basicSchema } from "prosemirror-schema-basic"; |
| 6 | import { |
| 7 | canInsertPageMentionAtRange, |
| 8 | canInsertPageMentions, |
| 9 | } from "@/client/components/editor/lib/page-mention/can-insert"; |
| 10 | import { getSlashMenuItems, filterItems } from "@/client/components/editor/controllers/slash/items"; |
| 11 | |
| 12 | const schema = new Schema({ |
| 13 | nodes: basicSchema.spec.nodes.addBefore("text", "pageMention", { |
| 14 | inline: true, |
| 15 | group: "inline", |
| 16 | atom: true, |
| 17 | selectable: true, |
| 18 | attrs: { pageId: { default: null } }, |
| 19 | toDOM(node) { |
| 20 | return ["span", { "data-page-id": node.attrs.pageId ?? "" }]; |
| 21 | }, |
| 22 | parseDOM: [{ tag: "span[data-page-id]" }], |
| 23 | }), |
| 24 | marks: basicSchema.spec.marks, |
| 25 | }); |
| 26 | |
| 27 | function createEditor(blockType: "paragraph" | "code_block"): Editor { |
| 28 | const content = schema.node(blockType, null, [schema.text("[[roadmap")]); |
| 29 | const doc = schema.node("doc", null, [content]); |
| 30 | const state = EditorState.create({ |
| 31 | schema, |
| 32 | doc, |
| 33 | selection: TextSelection.create(doc, content.content.size + 1), |
| 34 | }); |
| 35 | |
| 36 | return { |
| 37 | isEditable: true, |
| 38 | schema, |
| 39 | state, |
| 40 | } as unknown as Editor; |
| 41 | } |
| 42 | |
| 43 | function fullTextRange(editor: Editor) { |
| 44 | const contentSize = editor.state.doc.firstChild?.content.size ?? 0; |
| 45 | return { from: 1, to: contentSize + 1 }; |
| 46 | } |
| 47 | |
| 48 | describe("page mention insertion availability", () => { |
| 49 | it("allows mention insertion in inline text blocks", () => { |
| 50 | const editor = createEditor("paragraph"); |
| 51 | |
| 52 | expect(canInsertPageMentionAtRange(editor, fullTextRange(editor))).toBe(true); |
| 53 | }); |
| 54 | |
| 55 | it("blocks mention insertion inside code blocks", () => { |
| 56 | const editor = createEditor("code_block"); |
| 57 | |
| 58 | expect(canInsertPageMentionAtRange(editor, fullTextRange(editor))).toBe(false); |
| 59 | }); |
| 60 | |
| 61 | it("hides the slash-menu Link page item when the current selection cannot accept a mention", () => { |
| 62 | const paragraphEditor = createEditor("paragraph"); |
| 63 | const codeBlockEditor = createEditor("code_block"); |
| 64 | const items = getSlashMenuItems({ |
| 65 | pageMention: { openPicker: vi.fn() }, |
| 66 | image: { insertImage: vi.fn() }, |
| 67 | emoji: { openPicker: vi.fn() }, |
| 68 | ai: null, |
| 69 | }); |
| 70 | |
| 71 | const paragraphTitles = filterItems(items, "", { editor: paragraphEditor }).map((item) => item.title); |
| 72 | const codeBlockTitles = filterItems(items, "", { editor: codeBlockEditor }).map((item) => item.title); |
| 73 | |
| 74 | expect(paragraphTitles).toContain("Link page"); |
| 75 | expect(codeBlockTitles).not.toContain("Link page"); |
| 76 | }); |
| 77 | |
| 78 | it("keeps the shared-editor insertion gate unchanged", () => { |
| 79 | expect(canInsertPageMentions({ editable: true, workspaceId: "ws-1", shareToken: undefined })).toBe(true); |
| 80 | expect(canInsertPageMentions({ editable: true, workspaceId: "ws-1", shareToken: "share-token" })).toBe(false); |
| 81 | }); |
| 82 | }); |