80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
import { Editor } from "@tiptap/core";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { buildExtensions } from "@/components/MemoEditor/Editor/extensions";
|
|
import { filterSlashCommands, slashCommandItems } from "@/components/MemoEditor/Editor/SlashCommand";
|
|
|
|
let editor: Editor;
|
|
|
|
function applyCommand(name: string, content = "") {
|
|
editor = new Editor({ extensions: buildExtensions(), content, contentType: "markdown" });
|
|
const item = slashCommandItems.find((candidate) => candidate.name === name);
|
|
if (!item) throw new Error(`unknown slash command: ${name}`);
|
|
// Simulate a trigger at the end of an empty paragraph (the only case this
|
|
// range math is correct for). After inserting "/" into an empty paragraph,
|
|
// the doc is doc(paragraph(text"/")), with the slash at positions 1..2
|
|
// (doc.content.size = 3). We replicate that range so deleteRange removes the
|
|
// trigger character exactly as the real Suggestion plugin would.
|
|
editor.commands.insertContent("/");
|
|
const end = editor.state.doc.content.size;
|
|
item.apply(editor, { from: end - 2, to: end - 1 });
|
|
return editor.getMarkdown();
|
|
}
|
|
|
|
afterEach(() => editor?.destroy());
|
|
|
|
describe("WYSIWYG slash commands", () => {
|
|
it("todo converts the block into a task list", () => {
|
|
expect(applyCommand("todo").trimEnd()).toBe("- [ ]");
|
|
});
|
|
|
|
it("code converts the block into a code block", () => {
|
|
expect(applyCommand("code")).toContain("```");
|
|
});
|
|
|
|
it("link inserts a markdown link", () => {
|
|
expect(applyCommand("link")).toContain("[text](url)");
|
|
});
|
|
|
|
it("link places cursor selecting the display text", () => {
|
|
// After deleteRange(from:1, to:2) + insertContent the marked text node
|
|
// "text" occupies positions 1..5 (range.from=1, text length=4).
|
|
// The command chains .setTextSelection({ from: 1, to: 5 }) so the user
|
|
// can immediately overtype the placeholder label.
|
|
applyCommand("link");
|
|
const sel = editor.state.selection;
|
|
expect(sel.from).toBe(1);
|
|
expect(sel.to).toBe(5);
|
|
});
|
|
|
|
it("table inserts a preserved markdown table", () => {
|
|
const markdown = applyCommand("table");
|
|
expect(markdown).toContain("| Header | Header |");
|
|
expect(markdown).toContain("| ------ | ------ |");
|
|
});
|
|
|
|
it("table places cursor at the first header cell", () => {
|
|
// The preservedBlock text starts at range.from (pos 1). "| Header …" has
|
|
// "| " (2 chars) before the first label, so the command chains
|
|
// .setTextSelection(range.from + 2) = position 3, landing on the "H".
|
|
applyCommand("table");
|
|
const sel = editor.state.selection;
|
|
expect(sel.from).toBe(3);
|
|
expect(sel.to).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("filterSlashCommands", () => {
|
|
it("returns all items for an empty query", () => {
|
|
expect(filterSlashCommands("").map((i) => i.name)).toEqual(["todo", "code", "link", "table"]);
|
|
});
|
|
|
|
it("filters items by case-insensitive prefix", () => {
|
|
expect(filterSlashCommands("t").map((i) => i.name)).toEqual(["todo", "table"]);
|
|
expect(filterSlashCommands("T").map((i) => i.name)).toEqual(["todo", "table"]);
|
|
expect(filterSlashCommands("c").map((i) => i.name)).toEqual(["code"]);
|
|
});
|
|
|
|
it("returns an empty array for a non-matching query", () => {
|
|
expect(filterSlashCommands("x")).toEqual([]);
|
|
});
|
|
});
|