diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2f952b2..620e50427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### The desktop setup's question box waits for a typed character to be confirmed + +Japanese, Chinese and Korean are typed through an input method, where Enter confirms the character +being built. On the last setup screen that Enter also sent the question, with its last character +still unconfirmed. The box now waits for the character, the way the chat composer already does, and +an ordinary Enter still asks. + ### A malformed page size is refused instead of silently coerced `GET /channels` and `GET /api/admin/people` read `?limit=` with `Number.parseInt`, which diff --git a/desktop/src/App.test.tsx b/desktop/src/App.test.tsx index fc1060d8f..bb582f93d 100644 --- a/desktop/src/App.test.tsx +++ b/desktop/src/App.test.tsx @@ -1,6 +1,12 @@ import { afterAll, afterEach, beforeAll, expect, mock, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { act, cleanup, render, waitFor } from "@testing-library/react"; +import { + act, + cleanup, + fireEvent, + render, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { StrictMode } from "react"; @@ -131,6 +137,53 @@ test("setup records only model categories and reaches Ask when telemetry is unav expect(view.queryByText(/Nothing here leaves this computer/)).toBeNull(); }); +/** + * Setup finished and the question box on screen, focused as a person's typing would be. Call + * `useCompatibleEndpointSetup` first, as every test here does. + */ +async function reachAsk() { + const previous = invokeHandler; + invokeHandler = async (command, args) => { + if (command === "record_setup_event") return null; + if (command === "ask_the_bot") return "391"; + return previous(command, args); + }; + const view = await enterCompatibleEndpoint("https://models.example/v1"); + await userEvent.click(view.getByRole("button", { name: "Continue" })); + await userEvent.click(view.getByRole("button", { name: "Start OpenBot" })); + const question = await view.findByLabelText("Your question"); + // Keys go to the input that has focus, so the field is entered first. + await userEvent.click(question); + const asked = () => + invokeCalls.filter((call) => call.command === "ask_the_bot"); + return { question, asked }; +} + +test("the Enter that finishes a composed character does not ask the Bot", async () => { + useCompatibleEndpointSetup({}); + const { question, asked } = await reachAsk(); + + // Japanese, Chinese and Korean are typed through an input method, and Enter is how a character is + // confirmed. That Enter is still a keydown: Chromium marks it `isComposing`, and WebKit — the + // macOS webview — sends it after `compositionend` with the key code 229 instead. + await act(async () => { + fireEvent.keyDown(question, { key: "Enter", isComposing: true }); + fireEvent.keyDown(question, { key: "Enter", keyCode: 229 }); + }); + expect(asked()).toEqual([]); +}); + +test("an ordinary Enter in the question box asks the Bot, once", async () => { + // Unchanged by the test above, and pinned so it stays that way. + useCompatibleEndpointSetup({}); + const { question, asked } = await reachAsk(); + + await act(async () => { + fireEvent.keyDown(question, { key: "Enter", keyCode: 13 }); + }); + await waitFor(() => expect(asked()).toHaveLength(1)); +}); + type StartStackPayload = { root?: unknown; apiKey?: unknown; diff --git a/desktop/src/Ask.tsx b/desktop/src/Ask.tsx index 65a6e0eab..0e376cf58 100644 --- a/desktop/src/Ask.tsx +++ b/desktop/src/Ask.tsx @@ -70,7 +70,20 @@ export function Ask({ }} disabled={asking} onKeyDown={(event) => { - if (event.key === "Enter" && !asking) { + /* + * NOT THE ENTER THAT FINISHES A COMPOSED CHARACTER. Japanese, Chinese and Korean are + * typed through an input method, and Enter is how the character being built is + * confirmed. That press is still a keydown: Chromium marks it `isComposing`, and WebKit, + * which the macOS webview is, sends it after `compositionend` with the key code 229. Read + * as a plain Enter, it sent the question with the last character still unconfirmed. The + * chat composer's own Enter already skips it; this one did not. + */ + if ( + event.key === "Enter" && + !asking && + !event.nativeEvent.isComposing && + event.keyCode !== 229 + ) { ask(); } }}