From d3ac1a600cb85cd930721272e5e5bb6d3d4b68ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rouzbeh=E2=80=A0?= <78313022+rqzbeh@users.noreply.github.com> Date: Sun, 23 Aug 2026 02:43:10 +0330 Subject: [PATCH] refactor(dashboard): mirror check button disable state in AddApiKeyModal Enter handler (#10995) (#11156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked onto the current tip (authorship preserved), generated-count noise stripped. Pre-merge: file-size baseline rebaselined 1080→1082 with dated annotation (the +2 lines are the Enter-handler isCheckDisabled mirror — owner-requested #11056 polish; rest is Prettier reflow). Gate green; vitest add-api-key-modal-enter-key 2/2 (jsdom render test). Thank you @rqzbeh! --- config/quality/file-size-baseline.json | 3 +- .../[id]/components/modals/AddApiKeyModal.tsx | 80 +++++++------- .../ui/add-api-key-modal-enter-key.test.ts | 26 ----- .../ui/add-api-key-modal-enter-key.test.tsx | 104 ++++++++++++++++++ 4 files changed, 147 insertions(+), 66 deletions(-) delete mode 100644 tests/unit/ui/add-api-key-modal-enter-key.test.ts create mode 100644 tests/unit/ui/add-api-key-modal-enter-key.test.tsx diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 536318240a..b7f40cebfa 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -443,7 +443,8 @@ "src/shared/components/ModelSelectModal.tsx": 1138, "src/shared/constants/providers/apikey/gateways.ts": 1250 }, - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1080, + "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 1082, + "_rebaseline_2026_08_22_11156_enter_check_disabled": "PR #11156 (rqzbeh) own growth: AddApiKeyModal.tsx 1080->1082 (+2, Enter keydown handler now mirrors the isCheckDisabled condition — owner-requested post-merge polish from #11056; the rest of the diff is Prettier reflow). Covered by tests/unit/ui/add-api-key-modal-enter-key.test.tsx (jsdom render test, Enter dispatch assertions).", "src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051, "src/shared/components/ModelSelectModal.tsx": 1138, "src/shared/constants/providers/apikey/gateways.ts": 1298, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index 75917aec57..9a2bdc0038 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -757,46 +757,48 @@ export default function AddApiKeyModal({ onImport={(apiKey) => setFormData({ ...formData, apiKey })} /> )} - {!isNoAuthWebSessionCredential && ( -
- setFormData({ ...formData, apiKey: e.target.value })} - onKeyDown={(e) => { - if (e.key === "Enter" && !validating && !saving) { - e.preventDefault(); - handleValidate(); - } - }} - className="flex-1" - placeholder={apiCredentialPlaceholder} - hint={apiCredentialHint} - autoComplete="off" - spellCheck={false} - autoCapitalize="off" - /> -
- + {!isNoAuthWebSessionCredential && (() => { + const isCheckDisabled = + (!isCompatible && !apiKeyOptional && !formData.apiKey) || + (isGooglePse && !formData.cx.trim()) || + validating || + saving; + return ( +
+ setFormData({ ...formData, apiKey: e.target.value })} + onKeyDown={(e) => { + if (e.key === "Enter" && !isCheckDisabled) { + e.preventDefault(); + handleValidate(); + } + }} + className="flex-1" + placeholder={apiCredentialPlaceholder} + hint={apiCredentialHint} + autoComplete="off" + spellCheck={false} + autoCapitalize="off" + /> +
+ +
-
- )} + ); + })()} {isChatGptWebCodex && (
diff --git a/tests/unit/ui/add-api-key-modal-enter-key.test.ts b/tests/unit/ui/add-api-key-modal-enter-key.test.ts deleted file mode 100644 index 2532eb297e..0000000000 --- a/tests/unit/ui/add-api-key-modal-enter-key.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; - -describe("AddApiKeyModal Enter key submit (#10995)", () => { - it("AddApiKeyModal attaches onKeyDown Enter handler to the API Key input", () => { - const modalPath = path.resolve( - process.cwd(), - "src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx" - ); - const content = fs.readFileSync(modalPath, "utf8"); - assert.ok( - content.includes("onKeyDown"), - "AddApiKeyModal must contain onKeyDown event handler for Enter key validation" - ); - assert.ok( - content.includes('e.key === "Enter"'), - "onKeyDown handler must check for Enter key press" - ); - assert.ok( - content.includes("handleValidate()"), - "Enter key press must invoke handleValidate()" - ); - }); -}); diff --git a/tests/unit/ui/add-api-key-modal-enter-key.test.tsx b/tests/unit/ui/add-api-key-modal-enter-key.test.tsx new file mode 100644 index 0000000000..1c28a13230 --- /dev/null +++ b/tests/unit/ui/add-api-key-modal-enter-key.test.tsx @@ -0,0 +1,104 @@ +// @vitest-environment jsdom +// +// #10995 — Enter key in AddApiKeyModal triggers key validation without requiring a mouse click on Check. +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +const { default: AddApiKeyModal } = + await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal"); + +const containers: Array<{ root: ReturnType; el: HTMLDivElement }> = []; + +function render(props: Record) { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => { + root.render( + undefined} + onClose={() => {}} + {...(props as any)} + /> + ); + }); + containers.push({ root, el }); + return el; +} + +function setInputValue(input: HTMLInputElement, value: string) { + const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!; + act(() => { + setter.call(input, value); + input.dispatchEvent(new Event("input", { bubbles: true })); + }); +} + +function dispatchKeyDown(element: HTMLElement, key: string) { + act(() => { + element.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true })); + }); +} + +describe("AddApiKeyModal Enter key submit (#10995)", () => { + let originalFetch: typeof global.fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + for (const { root, el } of containers) { + act(() => root.unmount()); + el.remove(); + } + containers.length = 0; + }); + + it("does not trigger validation on Enter when input is empty", () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ valid: true }), + }); + global.fetch = fetchMock as any; + + const el = render({}); + const input = el.querySelector('input[type="password"]'); + expect(input).toBeTruthy(); + + dispatchKeyDown(input!, "Enter"); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("triggers validation on Enter key press when API key is provided", async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ valid: true }), + }); + global.fetch = fetchMock as any; + + const el = render({}); + const input = el.querySelector('input[type="password"]'); + expect(input).toBeTruthy(); + + setInputValue(input!, "sk-test1234567890"); + dispatchKeyDown(input!, "Enter"); + + expect(fetchMock).toHaveBeenCalledWith( + "/api/providers/validate", + expect.objectContaining({ + method: "POST", + body: expect.stringContaining("sk-test1234567890"), + }) + ); + }); +});