Files
multica/packages/core/utils.test.ts
Naiyuan Qing dce51e3a27 fix(views): guard IME composition on Enter-to-submit handlers (#2207)
* fix(views): guard IME composition on Enter-to-submit handlers

Chinese/Japanese/Korean IMEs use Enter to commit a multi-key
composition. When that Enter also triggers a submit/create handler,
the form fires before the user has finished typing.

Add a shared `isImeComposing` predicate in @multica/core/utils that
checks both `nativeEvent.isComposing` and `keyCode === 229` (Safari
clears isComposing on the commit keydown but keyCode stays 229).
Apply the guard to every Enter→action handler in packages/views where
the input can hold IME text: workspace name, agent name/description,
skill name, label name/edit, mention suggestion picker, property
picker search, delete-workspace typed confirmation.

Tiptap submit-shortcut already guards via `view.composing`; left as is.
Skipped numeric/email/URL/file-path inputs where IME does not apply.

Co-authored-by: multica-agent <github@multica.ai>

* style(agents): align Escape handling with early return in inspector

Three onKeyDown handlers in agent-detail-inspector.tsx now follow the same
shape as labels-panel: handle Escape with an explicit return, then the IME
guard, then Enter submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:17:35 +08:00

56 lines
2.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { createRequestId, createSafeId, generateUUID, isImeComposing } from "./utils";
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("utils id helpers", () => {
it("generateUUID returns a valid UUID v4", () => {
const id = generateUUID();
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
});
it("createSafeId falls back when crypto.randomUUID is unavailable", () => {
vi.stubGlobal("crypto", {
getRandomValues: (arr: Uint8Array) => {
for (let i = 0; i < arr.length; i++) arr[i] = i;
return arr;
},
});
const id = createSafeId();
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
});
it("createRequestId defaults to length 8 and respects custom length", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue("12345678-1234-4abc-8def-1234567890ab");
expect(createRequestId()).toBe("12345678");
expect(createRequestId(12)).toBe("123456781234");
});
});
describe("isImeComposing", () => {
it("returns true when nativeEvent.isComposing is set (React synthetic event)", () => {
expect(isImeComposing({ nativeEvent: { isComposing: true, keyCode: 13 } })).toBe(true);
});
it("returns true when nativeEvent.keyCode is 229 (Safari edge case)", () => {
// Safari clears isComposing on the keydown that ends composition; keyCode
// stays 229 throughout, which is the only reliable signal in that browser.
expect(isImeComposing({ nativeEvent: { isComposing: false, keyCode: 229 } })).toBe(true);
});
it("returns true for native KeyboardEvent without nativeEvent wrapper", () => {
expect(isImeComposing({ isComposing: true, keyCode: 13 })).toBe(true);
expect(isImeComposing({ isComposing: false, keyCode: 229 })).toBe(true);
});
it("returns false when not composing", () => {
expect(isImeComposing({ nativeEvent: { isComposing: false, keyCode: 13 } })).toBe(false);
expect(isImeComposing({ isComposing: false, keyCode: 13 })).toBe(false);
});
});