Skip to content
File

Blob: tests/client/components/editor/lib/insert-palette.test.ts

typescript132 lines
1import { describe, expect, it } from "vitest";
2import type { Editor } from "@tiptap/core";
3import { Schema } from "@tiptap/pm/model";
4import { EditorState, TextSelection } from "@tiptap/pm/state";
5import { schema as basicSchema } from "prosemirror-schema-basic";
6import { filterItems } from "@/client/components/editor/controllers/slash/items";
7import { createInsertPaletteItems } from "@/client/components/editor/lib/insert-palette";
8import type { EditorRuntimeSnapshot } from "@/client/components/editor/editor-runtime-context";
9import type { EditorAffordance } from "@/client/lib/affordance/editor";
10 
11const schema = new Schema({
12 nodes: basicSchema.spec.nodes.addBefore("text", "pageMention", {
13 inline: true,
14 group: "inline",
15 atom: true,
16 selectable: true,
17 attrs: { pageId: { default: null } },
18 toDOM(node) {
19 return ["span", { "data-page-id": node.attrs.pageId ?? "" }];
20 },
21 parseDOM: [{ tag: "span[data-page-id]" }],
22 }),
23 marks: basicSchema.spec.marks,
24});
25 
26function createEditor(blockType: "paragraph" | "code_block", editable = true): Editor {
27 const content = schema.node(blockType, null, [schema.text("[[roadmap")]);
28 const doc = schema.node("doc", null, [content]);
29 const state = EditorState.create({
30 schema,
31 doc,
32 selection: TextSelection.create(doc, content.content.size + 1),
33 });
34 
35 return {
36 isEditable: editable,
37 schema,
38 state,
39 } as unknown as Editor;
40}
41 
42function visibleTitles(
43 input?: Partial<{
44 runtime: EditorRuntimeSnapshot;
45 affordance: EditorAffordance;
46 blockType: "paragraph" | "code_block";
47 }>,
48) {
49 const runtime: EditorRuntimeSnapshot = input?.runtime ?? {
50 workspaceId: "ws-1",
51 pageId: "page-1",
52 shareToken: undefined,
53 };
54 const affordance: EditorAffordance = input?.affordance ?? {
55 documentEditable: true,
56 canInsertPageMentions: true,
57 canInsertImages: true,
58 canUseAiRewrite: false,
59 canUseAiGenerate: false,
60 canSummarizePage: false,
61 canAskPage: false,
62 };
63 const editor = createEditor(input?.blockType ?? "paragraph", affordance.documentEditable);
64 
65 return filterItems(
66 createInsertPaletteItems({
67 getRuntime: () => runtime,
68 getAffordance: () => affordance,
69 getPageMentionCandidates: () => [{ pageId: "page-2", title: "Roadmap", icon: null }],
70 }),
71 "",
72 { editor },
73 ).map((item) => item.title);
74}
75 
76describe("insert palette", () => {
77 it("keeps workspace paragraph insert affordances aligned", () => {
78 const titles = visibleTitles();
79 
80 expect(titles).toContain("Link page");
81 expect(titles).toContain("Image");
82 expect(titles).toContain("Emoji");
83 });
84 
85 it("hides Link page when the current selection cannot accept a mention", () => {
86 const titles = visibleTitles({ blockType: "code_block" });
87 
88 expect(titles).not.toContain("Link page");
89 expect(titles).toContain("Image");
90 });
91 
92 it("respects disabled insert affordances across entrypoints", () => {
93 const titles = visibleTitles({
94 affordance: {
95 documentEditable: true,
96 canInsertPageMentions: false,
97 canInsertImages: false,
98 canUseAiRewrite: false,
99 canUseAiGenerate: false,
100 canSummarizePage: false,
101 canAskPage: false,
102 },
103 });
104 
105 expect(titles).not.toContain("Link page");
106 expect(titles).not.toContain("Image");
107 expect(titles).toContain("Emoji");
108 });
109 
110 it("keeps image insertion hidden when runtime has no workspace context", () => {
111 const titles = visibleTitles({
112 runtime: {
113 workspaceId: undefined,
114 pageId: "page-1",
115 shareToken: undefined,
116 },
117 affordance: {
118 documentEditable: true,
119 canInsertPageMentions: true,
120 canInsertImages: true,
121 canUseAiRewrite: false,
122 canUseAiGenerate: false,
123 canSummarizePage: false,
124 canAskPage: false,
125 },
126 });
127 
128 expect(titles).toContain("Link page");
129 expect(titles).not.toContain("Image");
130 });
131});