memos/web/tests/editor-commands.test.ts
boojack 1e3ec38fa1 refactor(editor): modularize formatting and simplify editor state
Behavior-preserving restructure of the MemoEditor subsystem for simplicity
and extensibility:

- Add a data-driven command catalog (editorCommands.ts) as the single source
  for formatting verbs; the toolbar, active-state hook, and WYSIWYG handle all
  derive from it, so adding a verb is a one-file change.
- Add a createSuggestionExtension factory; TagSuggestion consumes it, so new
  `/` or `@` triggers reuse the shared popup in ~10 lines.
- Collapse FormattingController into a single EditorController with an optional
  `formatting` capability; PlainEditor is an honest textarea fallback (no faked
  formatting); replace stringly-typed isActive with typed getActiveFormats.
- Move audio-recorder state out of the reducer into useAudioRecorder, keeping a
  single recorderBusy flag in the store.
- Convert the editor context to an external store (useSyncExternalStore) with
  per-slice subscriptions, so typing no longer re-renders the toolbar, insert
  menu, or metadata.
- Extract the shared #tag lexing grammar (tag-grammar.ts) used by both the
  editor tokenizer and the remark renderer.
- Remove dead reducer actions/cases and fix a preview blob-URL leak on the
  upload path.

Add tests for the command catalog, suggestion factory, and autosave.
2026-06-22 23:51:59 +08:00

65 lines
2.3 KiB
TypeScript

import { Editor } from "@tiptap/core";
import { describe, expect, it } from "vitest";
import { EDITOR_COMMANDS, EDITOR_COMMANDS_BY_ID, getActiveFormats } from "@/components/MemoEditor/Editor/editorCommands";
import { buildExtensions } from "@/components/MemoEditor/Editor/extensions";
function makeEditor(content = "") {
return new Editor({ extensions: buildExtensions(), content, contentType: "markdown" });
}
describe("editor command catalog", () => {
it("exposes every command keyed by id", () => {
for (const command of EDITOR_COMMANDS) {
expect(EDITOR_COMMANDS_BY_ID[command.id]).toBe(command);
}
});
it("bold command toggles the bold mark, reflected by getActiveFormats", () => {
const editor = makeEditor("hello");
try {
editor.commands.selectAll();
expect(getActiveFormats(editor).bold).toBe(false);
EDITOR_COMMANDS_BY_ID.bold.run(editor);
expect(getActiveFormats(editor).bold).toBe(true);
} finally {
editor.destroy();
}
});
it("heading2 command sets the level, reported by getActiveFormats.headingLevel", () => {
const editor = makeEditor("title");
try {
expect(getActiveFormats(editor).headingLevel).toBeNull();
EDITOR_COMMANDS_BY_ID.heading2.run(editor);
expect(getActiveFormats(editor).headingLevel).toBe(2);
EDITOR_COMMANDS_BY_ID.paragraph.run(editor);
expect(getActiveFormats(editor).headingLevel).toBeNull();
} finally {
editor.destroy();
}
});
it("taskList command toggles a task list", () => {
const editor = makeEditor("buy milk");
try {
EDITOR_COMMANDS_BY_ID.taskList.run(editor);
expect(getActiveFormats(editor).taskList).toBe(true);
} finally {
editor.destroy();
}
});
it("link command applies a link from ctx.url and clears it when already active", () => {
const editor = makeEditor("memos");
try {
editor.commands.selectAll();
EDITOR_COMMANDS_BY_ID.link.run(editor, { url: "https://usememos.com" });
expect(getActiveFormats(editor).link).toBe(true);
editor.commands.selectAll();
EDITOR_COMMANDS_BY_ID.link.run(editor); // active → unset (ignores missing url)
expect(getActiveFormats(editor).link).toBe(false);
} finally {
editor.destroy();
}
});
});