Skip to content
Source
Sign inSign up

composer-list-split.browser.test.ts

100 lines3.5 KB

1import { Editor } from "@tiptap/core";
2import { TaskList } from "@tiptap/extension-task-list";
3import { TextSelection } from "@tiptap/pm/state";
4import StarterKit from "@tiptap/starter-kit";
5import { describe, expect, it } from "vite-plus/test";
6​
7import {
8 buildDocJson,
9 buildTiptapContent,
10 ComposerBlockExtensions,
11 ComposerCodeBlockExtension,
12 ComposerListExtensions,
13 ComposerTaskItemExtension,
14 serializeEditorDoc,
15} from "./composer-rich-text-doc";
16​
17// ProseMirror's DOM observer can flush on a timer after a test ends, which
18// throws once its DOM is gone, so every editor is acquired with `using`.
19function makeEditor(value: string): Editor & Disposable {
20 const editor = new Editor({
21 extensions: [
22 StarterKit.configure({
23 bulletList: false,
24 orderedList: false,
25 listItem: false,
26 codeBlock: false,
27 blockquote: false,
28 heading: false,
29 horizontalRule: false,
30 trailingNode: false,
31 listKeymap: false,
32 }),
33 ...ComposerListExtensions,
34 TaskList,
35 ComposerTaskItemExtension,
36 ComposerCodeBlockExtension,
37 ...ComposerBlockExtensions,
38 ],
39 content: buildDocJson(value, (name) => ({ label: name, description: null })),
40 });
41 return Object.assign(editor, { [Symbol.dispose]: () => editor.destroy() });
43​
44function placeCaret(editor: Editor, text: string, offset = 0) {
45 let caret = -1;
46 editor.state.doc.descendants((node, pos) => {
47 if (caret < 0 && node.isText && node.text!.includes(text)) {
48 caret = pos + node.text!.indexOf(text) + offset;
49 }
50 });
51 editor.view.dispatch(editor.state.tr.setSelection(TextSelection.create(editor.state.doc, caret)));
53​
54function storedDraft(editor: Editor) {
55 const stored = serializeEditorDoc(editor.state.doc).value;
56 using reloaded = makeEditor(stored);
57 expect(reloaded.state.doc.toString()).toBe(editor.state.doc.toString());
58 return stored;
60​
61describe("Backspace at the start of a line", () => {
62 it.each([
63 ["- a\n```\nl1\nl2\n```", "- a\nl1\nl2"],
64 ["p\n```ts\nl1\n```", "p\nl1"],
65 ])("removes the fence of %j rather than joining its code up", (value, expected) => {
66 using editor = makeEditor(value);
67 placeCaret(editor, "l1");
68 editor.commands.keyboardShortcut("Backspace");
69 expect(storedDraft(editor)).toBe(expected);
70 });
71​
72 // Neither item has a list of its own kind one level up to lift into.
73 it.each([
74 ["- a\n - [ ] x\n - [ ] y", "- a\n - [ ] xy"],
75 ["- [ ] a\n - x\n - y", "- [ ] a\n - xy"],
76 ["- a\n - [ ] y\n - [ ] q", "- ay\n - [ ] q"],
77 ])("joins y in %j onto the line above", (value, expected) => {
78 using editor = makeEditor(value);
79 placeCaret(editor, "y");
80 editor.commands.keyboardShortcut("Backspace");
81 expect(storedDraft(editor)).toBe(expected);
82 });
83​
84 it("removes an empty checkbox under a bullet", () => {
85 using editor = makeEditor("- a\n - [ ] x\n - [ ]");
86 editor.view.dispatch(editor.state.tr.setSelection(TextSelection.atEnd(editor.state.doc)));
87 editor.commands.keyboardShortcut("Backspace");
88 expect(storedDraft(editor)).toBe("- a\n - [ ] x");
89 });
90});
91​
92describe("blocks that a list item or quote cannot hold", () => {
93 it("puts lines pasted at the end of an item after the list", () => {
94 using editor = makeEditor("- a");
95 editor.view.dispatch(editor.state.tr.setSelection(TextSelection.atEnd(editor.state.doc)));
96 const lines = buildTiptapContent("one\ntwo", (name) => ({ label: name, description: null }));
97 editor.commands.insertContent(lines);
98 expect(storedDraft(editor)).toBe("- a\none\ntwo");
99 });
100});