mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
* 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>
56 lines
2.1 KiB
TypeScript
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);
|
|
});
|
|
});
|