diff --git a/docs/screenshots/command-code-add-account-paste.png b/docs/screenshots/command-code-add-account-paste.png new file mode 100644 index 0000000000..c73e7594c7 Binary files /dev/null and b/docs/screenshots/command-code-add-account-paste.png differ diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index 60c15225de..92815a7a1e 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -119,6 +119,10 @@ export default function ProviderAuthPanel({ const [addingKey, setAddingKey] = useState(false); const [newKey, setNewKey] = useState(""); const [keyBusy, setKeyBusy] = useState(false); + const [manualCode, setManualCode] = useState(""); + const [manualCodeBusy, setManualCodeBusy] = useState(false); + const [manualCodeMsg, setManualCodeMsg] = useState(""); + const [manualCodeOk, setManualCodeOk] = useState(true); const [importBusy, setImportBusy] = useState(false); const [importStatus, setImportStatus] = useState<"idle" | "invalid" | "failed" | "complete">("idle"); const [importResult, setImportResult] = useState(null); @@ -126,6 +130,21 @@ export default function ProviderAuthPanel({ const importFileRef = useRef(null); const deviceCodeCopy = useCopyFeedback(); + const resetManualCode = () => { + setManualCode(""); + setManualCodeMsg(""); + setManualCodeOk(true); + }; + + // Login hints are removed when a flow ends and change when a new flow starts. + // Do not retain credential-bearing input or feedback across either boundary. + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect, react/react-compiler + setManualCode(""); + setManualCodeMsg(""); + setManualCodeOk(true); + }, [item.name, loginHint?.provider, loginHint?.url, loginHint?.deviceCode]); + // Soft "a=1 enrichment lands after the local account list. Reserve stacked // bar height briefly so bars don't shove rows when WHAM returns. // @@ -195,6 +214,25 @@ export default function ProviderAuthPanel({ } }; + const submitManualCode = async () => { + const input = manualCode.trim(); + if (!input || manualCodeBusy || !authHandlers.onSubmitManualCode) return; + setManualCodeBusy(true); + setManualCodeMsg(""); + try { + await authHandlers.onSubmitManualCode(item.name, input); + setManualCode(""); + setManualCodeOk(true); + setManualCodeMsg(t("prov.pasteOk")); + } catch (error) { + setManualCodeOk(false); + const message = error instanceof Error && error.message.trim() ? error.message : t("prov.networkError"); + setManualCodeMsg(t("prov.pasteFail", { error: message })); + } finally { + setManualCodeBusy(false); + } + }; + const importCockpitFile = async (file: File | undefined) => { if (!file || importBusy) return; setImportBusy(true); @@ -326,8 +364,55 @@ export default function ProviderAuthPanel({ )} + {authHandlers.onSubmitManualCode && ( +
+
+ {item.name === "command-code" + ? t("prov.pasteCommandCodeHint") + : t("prov.pasteRedirectHint")} +
+
+ { setManualCode(e.target.value); setManualCodeMsg(""); }} + onKeyDown={e => { + if (e.key === "Enter" && manualCode.trim()) { + e.preventDefault(); + void submitManualCode(); + } + }} + placeholder={item.name === "command-code" ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")} + aria-label={item.name === "command-code" ? t("prov.pasteCommandCodePlaceholder") : t("prov.pasteRedirect")} + disabled={manualCodeBusy} + className="input text-label" + style={{ flex: 1 }} + /> + +
+ {manualCodeMsg && ( +
+ {manualCodeMsg} +
+ )} +
+ )} {authHandlers.onCancelLogin && ( - )} diff --git a/gui/src/components/provider-workspace/types.ts b/gui/src/components/provider-workspace/types.ts index b5757d06eb..ecce58b3f7 100644 --- a/gui/src/components/provider-workspace/types.ts +++ b/gui/src/components/provider-workspace/types.ts @@ -74,6 +74,8 @@ export type AccountLoadState = "idle" | "loading" | "ready" | "error"; export interface ProviderAuthHandlers { onLogin: (provider: string, addAccount?: boolean) => void | Promise; onCancelLogin?: (provider: string) => void; + /** Submit a pasted redirect URL / authorization code / raw API key to an in-progress login. */ + onSubmitManualCode?: (provider: string, input: string) => void | Promise; onLogout: (provider: string) => void | Promise; onReauth: (provider: string, accountId?: string) => void | Promise; onSwitchAccount: (provider: string, account: OAuthAccountRow) => void | Promise; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index a4a681d9b7..542ab23b03 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -379,7 +379,9 @@ export const de: Record = { "prov.aliasSaveFailed": "Alias konnte nicht gespeichert werden", "prov.accountId": "ID", "prov.pasteRedirect": "Redirect-URL oder Code einfügen", - "prov.pasteRedirectHint": "Zeigt der Browser einen localhost-Fehler, kopiere die vollständige URL aus der Adressleiste und füge sie hier ein (oder den Autorisierungscode).", + "prov.pasteRedirectHint": "Falls der Browser einen localhost-Fehler zeigt, kopieren Sie die vollständige URL aus der Adressleiste und fügen Sie sie hier ein (oder fügen Sie den Autorisierungscode ein).", + "prov.pasteCommandCodePlaceholder": "Command-Code-API-Schlüssel oder Redirect-URL einfügen", + "prov.pasteCommandCodeHint": "Fügen Sie einen Command-Code-API-Schlüssel (user_… aus ~/.commandcode/auth.json) ein, um ihn als weiteres Konto hinzuzufügen. Alternativ können Sie die Redirect-URL aus dem Browser einfügen.", "prov.pasteSubmit": "Senden", "prov.pasteSubmitting": "Wird gesendet…", "prov.pasteOk": "Code gesendet — Anmeldung wird abgeschlossen…", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6ed131e19c..27fb828647 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -403,6 +403,8 @@ export const en = { "prov.accountId": "ID", "prov.pasteRedirect": "Paste redirect URL or code", "prov.pasteRedirectHint": "If the browser shows a localhost error, copy the full URL from its address bar and paste it here (or paste the authorization code).", + "prov.pasteCommandCodePlaceholder": "Paste Command Code API key or redirect URL", + "prov.pasteCommandCodeHint": "Paste a Command Code API key (user_… from ~/.commandcode/auth.json) to add it as another account, or the redirect URL from the browser.", "prov.pasteSubmit": "Submit", "prov.pasteSubmitting": "Submitting…", "prov.pasteOk": "Code submitted — finishing login…", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 1382ed2ee9..34ac10361c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -390,6 +390,8 @@ export const fr: Record = { "prov.accountId": "ID", "prov.pasteRedirect": "Coller l’URL de redirection ou le code", "prov.pasteRedirectHint": "Si le navigateur affiche une erreur localhost, copiez l’URL complète depuis sa barre d’adresse et collez-la ici (ou collez le code d’autorisation).", + "prov.pasteCommandCodePlaceholder": "Coller la clé API Command Code ou l’URL de redirection", + "prov.pasteCommandCodeHint": "Collez une clé API Command Code (user_… depuis ~/.commandcode/auth.json) pour l’ajouter comme autre compte, ou l’URL de redirection du navigateur.", "prov.pasteSubmit": "Envoyer", "prov.pasteSubmitting": "Envoi…", "prov.pasteOk": "Code envoyé — finalisation de la connexion…", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 10f91145f2..c3c74960ca 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -385,7 +385,9 @@ export const ja: Record = { "prov.copyCode": "コードをコピー", "prov.codeCopied": "コードをコピーしました", "prov.pasteRedirect": "リダイレクト URL またはコードを貼り付け", - "prov.pasteRedirectHint": "ブラウザに localhost エラーが表示された場合、アドレスバーから URL 全体をコピーしてここに貼り付けてください(または認可コードを貼り付け)。", + "prov.pasteRedirectHint": "ブラウザに localhost エラーが表示された場合は、アドレスバーの完全な URL をコピーしてここに貼り付けてください(認可コードも可)。", + "prov.pasteCommandCodePlaceholder": "Command Code API キーまたはリダイレクト URL を貼り付け", + "prov.pasteCommandCodeHint": "Command Code API キー(~/.commandcode/auth.json の user_…)を貼り付けると別のアカウントとして追加されます。またはブラウザのリダイレクト URL を貼り付けます。", "prov.pasteSubmit": "送信", "prov.pasteSubmitting": "送信中…", "prov.pasteOk": "コードを送信しました — ログインを完了しています…", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b19fcad3e4..490cfa851a 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -388,7 +388,9 @@ export const ko: Record = { "prov.aliasSaveFailed": "별칭을 저장하지 못했습니다", "prov.accountId": "ID", "prov.pasteRedirect": "리다이렉트 URL 또는 코드 붙여넣기", - "prov.pasteRedirectHint": "브라우저에 localhost 오류가 표시되면, 주소창의 전체 URL을 복사해 여기에 붙여넣으세요(또는 인증 코드 붙여넣기).", + "prov.pasteRedirectHint": "브라우저에 localhost 오류가 표시되면 주소 표시줄의 전체 URL을 복사하여 여기에 붙여넣으세요(또는 인증 코드를 붙여넣으세요).", + "prov.pasteCommandCodePlaceholder": "Command Code API 키 또는 리다이렉트 URL 붙여넣기", + "prov.pasteCommandCodeHint": "Command Code API 키(~/.commandcode/auth.json의 user_…)를 붙여넣어 다른 계정으로 추가하거나 브라우저의 리다이렉트 URL을 붙여넣으세요.", "prov.pasteSubmit": "제출", "prov.pasteSubmitting": "제출 중…", "prov.pasteOk": "코드를 제출했습니다 — 로그인 완료 중…", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 252de8396c..d1d7a75a65 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -390,7 +390,9 @@ export const ru: Record = { "prov.aliasSaveFailed": "Не удалось сохранить псевдоним", "prov.accountId": "ID", "prov.pasteRedirect": "Вставьте URL перенаправления или код", - "prov.pasteRedirectHint": "Если браузер показывает ошибку localhost, скопируйте полный URL из его адресной строки и вставьте сюда (или вставьте код авторизации).", + "prov.pasteRedirectHint": "Если браузер показывает ошибку localhost, скопируйте полный URL из адресной строки и вставьте его сюда (или вставьте код авторизации).", + "prov.pasteCommandCodePlaceholder": "Вставьте API-ключ Command Code или URL перенаправления", + "prov.pasteCommandCodeHint": "Вставьте API-ключ Command Code (user_… из ~/.commandcode/auth.json), чтобы добавить его как ещё один аккаунт, или URL перенаправления из браузера.", "prov.pasteSubmit": "Отправить", "prov.pasteSubmitting": "Отправка…", "prov.pasteOk": "Код отправлен — завершаем вход…", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 7d50716b79..646e40e844 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -393,7 +393,9 @@ export const tr: Record = { "prov.aliasSaveFailed": "Takma ad kaydedilemedi", "prov.accountId": "ID", "prov.pasteRedirect": "Yönlendirme URL'sini veya kodu yapıştırın", - "prov.pasteRedirectHint": "Tarayıcı localhost hatası gösterirse adres çubuğundaki URL'yi kopyalayıp buraya yapıştırın.", + "prov.pasteRedirectHint": "Tarayıcı bir localhost hatası gösteriyorsa, adres çubuğundaki tam URL'yi kopyalayıp buraya yapıştırın (veya yetkilendirme kodunu yapıştırın).", + "prov.pasteCommandCodePlaceholder": "Command Code API anahtarını veya yönlendirme URL'sini yapıştırın", + "prov.pasteCommandCodeHint": "Başka bir hesap olarak eklemek için bir Command Code API anahtarı (user_…, ~/.commandcode/auth.json) veya tarayıcıdan yönlendirme URL'sini yapıştırın.", "prov.pasteSubmit": "Gönder", "prov.pasteSubmitting": "Gönderiliyor…", "prov.pasteOk": "Kod gönderildi — giriş tamamlanıyor…", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 9dd4ef7b69..0e13c0e030 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -280,7 +280,9 @@ export const zhTW: Record = { "prov.aliasSaveFailed": "無法儲存別名", "prov.accountId": "ID", "prov.pasteRedirect": "貼上重定向 URL 或授權碼", - "prov.pasteRedirectHint": "如果瀏覽器顯示 localhost 錯誤,請複製位址列中的完整 URL 並貼上到此處(或貼上授權碼)。", + "prov.pasteRedirectHint": "如果瀏覽器顯示 localhost 錯誤,請複製網址列的完整 URL 並貼到此處(或貼上授權碼)。", + "prov.pasteCommandCodePlaceholder": "貼上 Command Code API 金鑰或重新導向 URL", + "prov.pasteCommandCodeHint": "貼上 Command Code API 金鑰(~/.commandcode/auth.json 中的 user_…)以新增為另一個帳戶,或貼上瀏覽器中的重新導向 URL。", "prov.pasteSubmit": "提交", "prov.pasteSubmitting": "提交中…", "prov.pasteOk": "已提交授權碼 — 正在完成登入…", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 260b6a233f..7b999bb864 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -385,7 +385,9 @@ export const zh: Record = { "prov.aliasSaveFailed": "无法保存别名", "prov.accountId": "ID", "prov.pasteRedirect": "粘贴重定向 URL 或授权码", - "prov.pasteRedirectHint": "如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到此处(或粘贴授权码)。", + "prov.pasteRedirectHint": "如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到这里(或粘贴授权码)。", + "prov.pasteCommandCodePlaceholder": "粘贴 Command Code API 密钥或重定向 URL", + "prov.pasteCommandCodeHint": "粘贴 Command Code API 密钥(~/.commandcode/auth.json 中的 user_…)以添加为另一个账户,或粘贴浏览器中的重定向 URL。", "prov.pasteSubmit": "提交", "prov.pasteSubmitting": "提交中…", "prov.pasteOk": "已提交代码 — 正在完成登录…", diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx index 6c23f43e70..04f7bffd9b 100644 --- a/gui/src/pages/Providers.tsx +++ b/gui/src/pages/Providers.tsx @@ -211,7 +211,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { const bumpModelsRefresh = () => setModelsRefreshToken(n => n + 1); - const { cancelLoginOAuth, loginOAuth, logoutOAuth } = useProvidersOAuth({ + const { cancelLoginOAuth, loginOAuth, logoutOAuth, submitManualCode } = useProvidersOAuth({ apiBase, t, aliveRef, accountSets, setAccountSets, setBusy, setStatus, setLoginInfo, setOauthStatus, notify, fetchConfig, fetchOauth, fetchAccountSets, fetchProviderQuotas, bumpModelsRefresh, @@ -365,6 +365,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { authHandlers={{ onLogin: requestLoginOAuth, onCancelLogin: cancelLoginOAuth, + onSubmitManualCode: submitManualCode, onLogout: logoutOAuth, onReauth: (provider, accountId) => loginOAuth(provider, true, accountId), onSwitchAccount: switchAccount, diff --git a/gui/src/pages/use-providers-oauth.ts b/gui/src/pages/use-providers-oauth.ts index 456d33e61a..26905c589b 100644 --- a/gui/src/pages/use-providers-oauth.ts +++ b/gui/src/pages/use-providers-oauth.ts @@ -211,5 +211,24 @@ export function useProvidersOAuth({ } }; - return { cancelLoginOAuth, loginOAuth, logoutOAuth }; + const submitManualCode = async (provider: string, input: string) => { + try { + const res = await fetch(`${apiBase}/api/oauth/login/code`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, input }), + }); + if (!aliveRef.current) return; + if (!res.ok) { + const data = await res.json().catch(() => ({})) as { error?: string }; + throw new Error(data.error || res.statusText); + } + } catch (error) { + if (aliveRef.current) { + throw error instanceof Error ? error : new Error(String(error)); + } + } + }; + + return { cancelLoginOAuth, loginOAuth, logoutOAuth, submitManualCode }; } diff --git a/gui/src/styles/provider-workspace-settings.css b/gui/src/styles/provider-workspace-settings.css index 3429106e7f..ff2c864488 100644 --- a/gui/src/styles/provider-workspace-settings.css +++ b/gui/src/styles/provider-workspace-settings.css @@ -30,6 +30,7 @@ .pwi-auth-wait-title { font-size: var(--text-control); font-weight: 600; color: var(--text); } .pwi-auth-wait-copy { font-size: var(--text-label); color: var(--muted); line-height: 1.45; } .pwi-device-code-wrap { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px; margin: 6px 0; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); } +.pwi-auth-paste { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; } .pwi-device-code { font-size: 20px; font-weight: 800; letter-spacing: .14em; color: var(--text); user-select: all; } .pwi-auth-row-copy { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; } .pwi-auth-row-secondary { max-width: 100%; overflow: hidden; text-overflow: ellipsis; font-size: var(--text-label); color: var(--muted); } diff --git a/gui/tests/provider-auth-manual-code.test.tsx b/gui/tests/provider-auth-manual-code.test.tsx new file mode 100644 index 0000000000..9714562839 --- /dev/null +++ b/gui/tests/provider-auth-manual-code.test.tsx @@ -0,0 +1,142 @@ +import { afterEach, beforeEach, expect, mock, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { LanguageProvider } from "../src/i18n/provider"; +import ProviderAuthPanel from "../src/components/provider-workspace/ProviderAuthPanel"; +import type { ProviderAuthHandlers } from "../src/components/provider-workspace/types"; +import type { WorkspaceItem } from "../src/provider-workspace/catalog"; + +const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; +let previous: Record<(typeof globals)[number], unknown>; +let win: Window; +let host: HTMLElement; +let root: Root | null = null; +let submit: (provider: string, input: string) => Promise; +let rejection: unknown; + +const item: WorkspaceItem = { + name: "command-code", + adapter: "command-code", + baseUrl: "https://api.commandcode.ai", + authMode: "oauth", +}; + +beforeEach(() => { + previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous; + win = new Window({ url: "http://localhost/" }); + Object.defineProperty(win.navigator, "language", { configurable: true, value: "en-US" }); + rejection = new Error("invalid authorization code"); + submit = mock(async () => { + if (rejection) throw rejection; + }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: win.document }, window: { configurable: true, value: win }, + navigator: { configurable: true, value: win.navigator }, localStorage: { configurable: true, value: win.localStorage }, + }); + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + host = win.document.createElement("div") as unknown as HTMLElement; + win.document.body.appendChild(host as never); +}); + +afterEach(async () => { + if (root) { const current = root; await act(async () => { current.unmount(); }); root = null; } + for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); + await win.happyDOM?.close?.(); +}); + +test("masks pasted Command Code credentials and preserves rejection feedback", async () => { + // React DOM must bind to the happy-dom globals installed in beforeEach. + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( + + {}, onLogout: () => {}, onReauth: () => {}, onSwitchAccount: () => {}, onRemoveAccount: () => {}, + onAddApiKey: async () => true, onSwitchApiKey: () => {}, onRemoveApiKey: () => {}, onEditAlias: () => {}, + onSubmitManualCode: submit, + }} + /> + , + ); + }); + + const input = host.querySelector('input[type="password"]') as HTMLInputElement; + expect(input).toBeTruthy(); + expect(input.type).toBe("password"); + await act(async () => { + const setter = Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value")!.set!; + setter.call(input, "user_secret"); + input.dispatchEvent(new win.Event("input", { bubbles: true })); + }); + expect(input.value).toBe("user_secret"); + expect((host.querySelector(".pwi-auth-paste button") as HTMLButtonElement).disabled).toBe(false); + await act(async () => { + (host.querySelector(".pwi-auth-paste button") as HTMLButtonElement).click(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(submit).toHaveBeenCalledWith("command-code", "user_secret"); + expect(host.textContent).toContain("invalid authorization code"); + expect(host.querySelector('[role="alert"]')?.textContent).toContain("invalid authorization code"); + + rejection = {}; + await act(async () => { + (host.querySelector(".pwi-auth-paste button") as HTMLButtonElement).click(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(host.textContent).toContain("Could not submit code: Network error. Check that the proxy is running and try again."); + expect(host.querySelector('[role="alert"]')?.textContent).toContain("Network error"); + + rejection = null; + await act(async () => { + (host.querySelector(".pwi-auth-paste button") as HTMLButtonElement).click(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(host.querySelector('[role="status"]')?.textContent).toContain("Code submitted — finishing login…"); + + // Ending the flow must clear the credential and its feedback even when the + // provider panel remains mounted for the next Add account attempt. + await act(async () => { + root!.render( + + {}, onLogout: () => {}, onReauth: () => {}, onSwitchAccount: () => {}, onRemoveAccount: () => {}, + onAddApiKey: async () => true, onSwitchApiKey: () => {}, onRemoveApiKey: () => {}, onEditAlias: () => {}, + onSubmitManualCode: submit, + }} + /> + , + ); + }); + expect(host.querySelector('input[type="password"]')).toBeNull(); + await act(async () => { + root!.render( + + {}, onLogout: () => {}, onReauth: () => {}, onSwitchAccount: () => {}, onRemoveAccount: () => {}, + onAddApiKey: async () => true, onSwitchApiKey: () => {}, onRemoveApiKey: () => {}, onEditAlias: () => {}, + onSubmitManualCode: submit, + }} + /> + , + ); + }); + expect((host.querySelector('input[type="password"]') as HTMLInputElement).value).toBe(""); + expect(host.querySelector('[role="status"]')).toBeNull(); +});