From addbf5a15740aa8cd4d88dad6fb0bf0894d5aace Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:45:28 -0500 Subject: [PATCH 1/5] test(web): cover composition in add-project inputs Co-Authored-By: GPT-6.1 Sol --- ...ommandPalette.composition.browser.test.tsx | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 apps/web/src/components/CommandPalette.composition.browser.test.tsx diff --git a/apps/web/src/components/CommandPalette.composition.browser.test.tsx b/apps/web/src/components/CommandPalette.composition.browser.test.tsx new file mode 100644 index 000000000000..5ac489cd61e5 --- /dev/null +++ b/apps/web/src/components/CommandPalette.composition.browser.test.tsx @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; +import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; +import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; +import { act, render } from "../../test/render"; +import { CommandPalette } from "./CommandPalette"; +import { openCommandPalette } from "../commandPaletteBus"; +import { primaryServerKeybindingsAtom } from "../state/server"; + +vi.mock("@effect/atom-react", async (original) => ({ + ...(await original()), + useAtomValue: (atom: unknown) => + atom === primaryServerKeybindingsAtom ? DEFAULT_RESOLVED_KEYBINDINGS : [], +})); +vi.mock("../hooks/useTheme", () => ({ + useRenderedAppearance: () => "light", + useTheme: () => ({ theme: "default", resolvedTheme: "light", appearanceMode: "light" }), +})); +vi.mock("../hooks/useCustomThemes", () => ({ useCustomThemes: () => [] })); +vi.mock("../hooks/useEnvironmentTheme", () => ({ useEnvironmentThemeDefinitions: () => [] })); +vi.mock("../hooks/useSettings", async (original) => ({ + ...(await original()), + useClientSettings: () => DEFAULT_CLIENT_SETTINGS, +})); +vi.mock("../hooks/useScratchProject", () => ({ + useScratchProject: () => ({ scratchEnvironmentId: () => null }), +})); +vi.mock("../hooks/useNewProject", () => ({ useNewProject: () => async () => undefined })); +vi.mock("../hooks/useHandleNewThread", () => ({ + useHandleNewThread: () => ({ handleNewThread: async () => undefined }), +})); +vi.mock("../hooks/useOpenPanelPullRequestUrl", () => ({ useOpenPanelPullRequestUrl: () => null })); +vi.mock("../connection/useDesktopLocalBootstraps", () => ({ useDesktopLocalBootstraps: () => [] })); +vi.mock("../state/use-atom-command", () => ({ + useAtomCommand: () => async () => ({ _tag: "Success", value: undefined }), +})); +vi.mock("../state/use-atom-query-runner", () => ({ + useAtomQueryRunner: () => async () => ({ _tag: "Success", value: undefined }), +})); +vi.mock("../state/environments", async (original) => ({ + ...(await original()), + useEnvironments: () => ({ + environments: [{ + environmentId: "local", + label: "Local", + connection: { phase: "connected" }, + entry: { target: { _tag: "PrimaryConnectionTarget" } }, + serverConfig: { + environment: { platform: { os: "linux" }, capabilities: { projectCloneTracking: true } }, + settings: { addProjectBaseDirectory: "/workspace/" }, + }, + }], + }), + usePrimaryEnvironmentId: () => "local", + useEnvironmentProviders: () => new Map(), +})); +vi.mock("../state/entities", async (original) => ({ + ...(await original()), + useProjects: () => [], + useThreadShells: () => [], + useServerConfigs: () => new Map(), +})); +vi.mock("../state/queries", async (original) => ({ + ...(await original()), + useThreadSearch: () => ({ matches: [] }), +})); +vi.mock("../state/session", async (original) => ({ + ...(await original()), + useEnvironmentScope: () => true, + readEnvironmentScope: () => true, +})); +vi.mock("../state/filesystem", async (original) => ({ + ...(await original()), + useFilesystemReadAccess: () => ({ allowed: true }), +})); +vi.mock("../state/query", async (original) => ({ + ...(await original()), + useEnvironmentQuery: () => ({ data: undefined, isLoading: false }), +})); +vi.mock("./settings/useAvailableSettingsSearchItems", () => ({ + useAvailableSettingsSearchItems: () => [], +})); + +// The real palette and Base UI input must agree that confirming composition is typing. +describe.each(["repository", "folder", "destination"] as const)("add-project %s", (step) => { + it.each([ + { isComposing: true, keyCode: 13 }, + { isComposing: false, keyCode: 229 }, + ])("keeps Enter in the input during composition: %j", async (composition) => { + await using _screen = await render({null}); + await act(() => openCommandPalette({ open: "add-project" })); + await page.getByText(step === "folder" ? "Local folder" : "Clone from URL", { exact: true }).click(); + const input = () => page.getByRole("combobox"); + if (step === "destination") { + await input().fill("https://example.com/team/project.git"); + await userEvent.keyboard("{Enter}"); + } + const value = step === "repository" ? "https://example.com/team/計画.git" : "/workspace/計画"; + await input().fill(value); + const event = new KeyboardEvent("keydown", { + key: "Enter", bubbles: true, cancelable: true, ...composition, + }); + await act(() => input().element().dispatchEvent(event)); + expect(event.defaultPrevented).toBe(false); + await expect.element(input()).toHaveValue(value); + if (step === "repository") { + await userEvent.keyboard("{Enter}"); + await expect.element(page.getByText("Choose a destination path and press Enter to clone.")).toBeVisible(); + } else { + await userEvent.keyboard("{Enter}"); + await expect.element(page.getByRole("combobox")).not.toBeInTheDocument(); + } + }); +}); From 725ffa09a971eb84b7564378a40d4209dcca285f Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:46:43 -0500 Subject: [PATCH 2/5] test(web): use current add-project source labels Co-Authored-By: GPT-6.1 Sol --- .../components/CommandPalette.composition.browser.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/CommandPalette.composition.browser.test.tsx b/apps/web/src/components/CommandPalette.composition.browser.test.tsx index 5ac489cd61e5..860137978809 100644 --- a/apps/web/src/components/CommandPalette.composition.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.composition.browser.test.tsx @@ -59,6 +59,7 @@ vi.mock("../state/entities", async (original) => ({ useProjects: () => [], useThreadShells: () => [], useServerConfigs: () => new Map(), + waitForProject: async () => undefined, })); vi.mock("../state/queries", async (original) => ({ ...(await original()), @@ -71,7 +72,7 @@ vi.mock("../state/session", async (original) => ({ })); vi.mock("../state/filesystem", async (original) => ({ ...(await original()), - useFilesystemReadAccess: () => ({ allowed: true }), + useFilesystemReadAccess: () => ({ canReadFiles: true, isPending: false, error: null }), })); vi.mock("../state/query", async (original) => ({ ...(await original()), @@ -89,7 +90,7 @@ describe.each(["repository", "folder", "destination"] as const)("add-project %s" ])("keeps Enter in the input during composition: %j", async (composition) => { await using _screen = await render({null}); await act(() => openCommandPalette({ open: "add-project" })); - await page.getByText(step === "folder" ? "Local folder" : "Clone from URL", { exact: true }).click(); + await page.getByText(step === "folder" ? "Local folder" : "Git URL", { exact: true }).click(); const input = () => page.getByRole("combobox"); if (step === "destination") { await input().fill("https://example.com/team/project.git"); From 1037f729f462af1c2ac8aca6c9a7f59be5b142f0 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:56:45 -0500 Subject: [PATCH 3/5] fix(web): keep add-project inputs open during IME composition Co-Authored-By: GPT-6.1 Sol --- ...ommandPalette.composition.browser.test.tsx | 29 ++++++++++++------- apps/web/src/components/CommandPalette.tsx | 6 ++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/CommandPalette.composition.browser.test.tsx b/apps/web/src/components/CommandPalette.composition.browser.test.tsx index 860137978809..3bbee55eb86a 100644 --- a/apps/web/src/components/CommandPalette.composition.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.composition.browser.test.tsx @@ -40,16 +40,18 @@ vi.mock("../state/use-atom-query-runner", () => ({ vi.mock("../state/environments", async (original) => ({ ...(await original()), useEnvironments: () => ({ - environments: [{ - environmentId: "local", - label: "Local", - connection: { phase: "connected" }, - entry: { target: { _tag: "PrimaryConnectionTarget" } }, - serverConfig: { - environment: { platform: { os: "linux" }, capabilities: { projectCloneTracking: true } }, - settings: { addProjectBaseDirectory: "/workspace/" }, + environments: [ + { + environmentId: "local", + label: "Local", + connection: { phase: "connected" }, + entry: { target: { _tag: "PrimaryConnectionTarget" } }, + serverConfig: { + environment: { platform: { os: "linux" }, capabilities: { projectCloneTracking: true } }, + settings: { addProjectBaseDirectory: "/workspace/" }, + }, }, - }], + ], }), usePrimaryEnvironmentId: () => "local", useEnvironmentProviders: () => new Map(), @@ -99,14 +101,19 @@ describe.each(["repository", "folder", "destination"] as const)("add-project %s" const value = step === "repository" ? "https://example.com/team/計画.git" : "/workspace/計画"; await input().fill(value); const event = new KeyboardEvent("keydown", { - key: "Enter", bubbles: true, cancelable: true, ...composition, + key: "Enter", + bubbles: true, + cancelable: true, + ...composition, }); await act(() => input().element().dispatchEvent(event)); expect(event.defaultPrevented).toBe(false); await expect.element(input()).toHaveValue(value); if (step === "repository") { await userEvent.keyboard("{Enter}"); - await expect.element(page.getByText("Choose a destination path and press Enter to clone.")).toBeVisible(); + await expect + .element(page.getByText("Choose a destination path and press Enter to clone.")) + .toBeVisible(); } else { await userEvent.keyboard("{Enter}"); await expect.element(page.getByRole("combobox")).not.toBeInTheDocument(); diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 0d2b480e89c9..baff1529bd79 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -2314,6 +2314,12 @@ function OpenCommandPaletteDialog(props: { } function handleKeyDown(event: KeyboardEvent): void { + if (event.key === "Enter" && (event.nativeEvent.isComposing || event.keyCode === 229)) { + // Confirming an IME candidate must not submit a path or select a row. + (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); + return; + } + const command = resolveShortcutCommand(event, keybindings, { platform: navigator.platform, context: { modelPickerOpen: false, editableFocus: true }, From 04da01d85c079b54de6623ed9b5c82c7a049b642 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 17:01:11 -0500 Subject: [PATCH 4/5] test(web): check the clone destination transition directly Co-Authored-By: GPT-6.1 Sol --- .../components/CommandPalette.composition.browser.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/CommandPalette.composition.browser.test.tsx b/apps/web/src/components/CommandPalette.composition.browser.test.tsx index 3bbee55eb86a..9dae805756df 100644 --- a/apps/web/src/components/CommandPalette.composition.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.composition.browser.test.tsx @@ -111,9 +111,7 @@ describe.each(["repository", "folder", "destination"] as const)("add-project %s" await expect.element(input()).toHaveValue(value); if (step === "repository") { await userEvent.keyboard("{Enter}"); - await expect - .element(page.getByText("Choose a destination path and press Enter to clone.")) - .toBeVisible(); + await expect.element(input()).toHaveValue("/workspace/計画"); } else { await userEvent.keyboard("{Enter}"); await expect.element(page.getByRole("combobox")).not.toBeInTheDocument(); From e4c51a9a19419b2f2cf2c088745b3834646bb3e1 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:48:37 +0000 Subject: [PATCH 5/5] test(web): keep the composition test's mocks current with main --- .../CommandPalette.composition.browser.test.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/CommandPalette.composition.browser.test.tsx b/apps/web/src/components/CommandPalette.composition.browser.test.tsx index 9dae805756df..478788590598 100644 --- a/apps/web/src/components/CommandPalette.composition.browser.test.tsx +++ b/apps/web/src/components/CommandPalette.composition.browser.test.tsx @@ -14,7 +14,7 @@ vi.mock("@effect/atom-react", async (original) => ({ })); vi.mock("../hooks/useTheme", () => ({ useRenderedAppearance: () => "light", - useTheme: () => ({ theme: "default", resolvedTheme: "light", appearanceMode: "light" }), + useTheme: () => ({ theme: "t3-chat", resolvedTheme: "light", appearanceMode: "light" }), })); vi.mock("../hooks/useCustomThemes", () => ({ useCustomThemes: () => [] })); vi.mock("../hooks/useEnvironmentTheme", () => ({ useEnvironmentThemeDefinitions: () => [] })); @@ -27,7 +27,13 @@ vi.mock("../hooks/useScratchProject", () => ({ })); vi.mock("../hooks/useNewProject", () => ({ useNewProject: () => async () => undefined })); vi.mock("../hooks/useHandleNewThread", () => ({ - useHandleNewThread: () => ({ handleNewThread: async () => undefined }), + useHandleNewThread: () => ({ + activeDraftThread: null, + activeThread: null, + defaultProjectRef: null, + handleNewThread: async () => undefined, + }), + useNewThreadHandler: () => async () => undefined, })); vi.mock("../hooks/useOpenPanelPullRequestUrl", () => ({ useOpenPanelPullRequestUrl: () => null })); vi.mock("../connection/useDesktopLocalBootstraps", () => ({ useDesktopLocalBootstraps: () => [] })); @@ -65,7 +71,11 @@ vi.mock("../state/entities", async (original) => ({ })); vi.mock("../state/queries", async (original) => ({ ...(await original()), - useThreadSearch: () => ({ matches: [] }), + useThreadSearch: (_environmentIds: unknown, query: string) => ({ + matches: [], + query, + isPending: false, + }), })); vi.mock("../state/session", async (original) => ({ ...(await original()),