Files
multica/packages/views/editor/extensions/submit-shortcut.test.ts
Jiayuan Zhang 932bbf2bb5 fix(editor): guard Mod-Enter submit against open IME composition (#5231)
The bare-Enter submit path already refuses to fire while view.composing
is true, but Mod-Enter had no such guard. Pressing ⌘↵ while a pinyin/kana
composition is still open submits the document WITHOUT the composed text
— e.g. paste a screenshot, type a Chinese sentence, hit ⌘↵ before the
buffer commits, and the submission carries only the screenshot. Apply the
same composing guard to Mod-Enter.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-10 22:44:23 +08:00

105 lines
3.1 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { getExtensionField } from "@tiptap/core";
import type { Editor } from "@tiptap/core";
import { createSubmitExtension } from "./submit-shortcut";
function getShortcuts(
ext: ReturnType<typeof createSubmitExtension>,
editor: Partial<Editor>,
): Record<string, () => boolean> {
const fn = getExtensionField<
() => Record<string, () => boolean>
>(ext, "addKeyboardShortcuts", {
name: "submitShortcut",
options: {},
storage: {},
editor: editor as Editor,
type: null,
});
return fn?.() ?? {};
}
describe("createSubmitExtension", () => {
const baseEditor = {
view: { composing: false } as unknown as Editor["view"],
isActive: () => false,
} as Partial<Editor>;
it("Mod-Enter submits when no composition is open", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: false }),
baseEditor,
);
expect(shortcuts["Mod-Enter"]).toBeDefined();
expect(shortcuts["Mod-Enter"]!()).toBe(true);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("Mod-Enter is suppressed during IME composition", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: false }),
{
view: { composing: true } as unknown as Editor["view"],
isActive: () => false,
},
);
expect(shortcuts["Mod-Enter"]!()).toBe(false);
expect(onSubmit).not.toHaveBeenCalled();
});
it("bare Enter is not bound when submitOnEnter is false", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: false }),
baseEditor,
);
expect(shortcuts.Enter).toBeUndefined();
expect(onSubmit).not.toHaveBeenCalled();
});
it("bare Enter submits when submitOnEnter is true", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
baseEditor,
);
expect(shortcuts.Enter).toBeDefined();
expect(shortcuts.Enter!()).toBe(true);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("Enter is suppressed during IME composition", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
{
view: { composing: true } as unknown as Editor["view"],
isActive: () => false,
},
);
expect(shortcuts.Enter!()).toBe(false);
expect(onSubmit).not.toHaveBeenCalled();
});
it("Enter is suppressed inside a code block", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
{
view: { composing: false } as unknown as Editor["view"],
isActive: (name: string) => name === "codeBlock",
},
);
expect(shortcuts.Enter!()).toBe(false);
expect(onSubmit).not.toHaveBeenCalled();
});
});