mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(codex): handle native login availability checks
This commit is contained in:
1 parent
3d126f458a
commit
a6594f8ae6
9 files changed
+308
-12
No files matched your search
@@ -95,6 +95,9 @@ interface CodexFormFieldsProps {
|
||||
codexOauthNoneOptionDescription?: string;
|
||||
codexOauthAllowUnboundSelection?: boolean;
|
||||
codexOauthAllowUnboundSelectionWithoutStatus?: boolean;
|
||||
codexOauthUnboundSelectionLoading?: boolean;
|
||||
codexOauthUnboundSelectionError?: boolean;
|
||||
onCodexOauthUnboundSelectionRetry?: () => void;
|
||||
codexOauthNativeLoginOnly?: boolean;
|
||||
codexOauthRequireExplicitSelection?: boolean;
|
||||
|
||||
@@ -389,6 +392,9 @@ export function CodexFormFields({
|
||||
codexOauthNoneOptionDescription,
|
||||
codexOauthAllowUnboundSelection,
|
||||
codexOauthAllowUnboundSelectionWithoutStatus,
|
||||
codexOauthUnboundSelectionLoading,
|
||||
codexOauthUnboundSelectionError,
|
||||
onCodexOauthUnboundSelectionRetry,
|
||||
codexOauthNativeLoginOnly,
|
||||
codexOauthRequireExplicitSelection,
|
||||
shouldShowSpeedTest,
|
||||
@@ -739,6 +745,9 @@ export function CodexFormFields({
|
||||
allowUnboundSelectionWithoutStatus={
|
||||
codexOauthAllowUnboundSelectionWithoutStatus
|
||||
}
|
||||
unboundSelectionLoading={codexOauthUnboundSelectionLoading}
|
||||
unboundSelectionError={codexOauthUnboundSelectionError}
|
||||
onUnboundSelectionRetry={onCodexOauthUnboundSelectionRetry}
|
||||
nativeLoginOnly={codexOauthNativeLoginOnly}
|
||||
requireExplicitSelection={codexOauthRequireExplicitSelection}
|
||||
/>
|
||||
|
||||
@@ -56,6 +56,12 @@ interface CodexOAuthSectionProps {
|
||||
allowUnboundSelection?: boolean;
|
||||
/** 不绑定选项不依赖托管账号状态,可在状态加载失败时继续选择 */
|
||||
allowUnboundSelectionWithoutStatus?: boolean;
|
||||
/** 是否仍在确认不绑定选项可用;加载期间保留选项但禁止选择 */
|
||||
unboundSelectionLoading?: boolean;
|
||||
/** 无法确认不绑定选项是否可用 */
|
||||
unboundSelectionError?: boolean;
|
||||
/** 重新确认不绑定选项是否可用 */
|
||||
onUnboundSelectionRetry?: () => void;
|
||||
/** 固定展示原生 Codex 当前登录,不允许改绑 */
|
||||
nativeLoginOnly?: boolean;
|
||||
/** 新建官方卡时不预选登录方式,要求用户明确选择 */
|
||||
@@ -86,6 +92,9 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
noneOptionDescription,
|
||||
allowUnboundSelection = true,
|
||||
allowUnboundSelectionWithoutStatus = false,
|
||||
unboundSelectionLoading = false,
|
||||
unboundSelectionError = false,
|
||||
onUnboundSelectionRetry,
|
||||
nativeLoginOnly = false,
|
||||
requireExplicitSelection = false,
|
||||
fastModeEnabled = false,
|
||||
@@ -203,6 +212,8 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
(allowUnboundSelection
|
||||
? (noneOptionLabel ?? t("codexOauth.useDefaultAccount", "使用默认账号"))
|
||||
: t("codexOauth.selectAccountPlaceholder", "选择一个 ChatGPT 账号"));
|
||||
const unboundSelectionUnavailable =
|
||||
unboundSelectionLoading || unboundSelectionError;
|
||||
|
||||
const accountSelect = (isStatusSuccess ||
|
||||
(allowUnboundSelection && allowUnboundSelectionWithoutStatus)) &&
|
||||
@@ -304,10 +315,18 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
{allowUnboundSelection && (
|
||||
<SelectItem
|
||||
value="none"
|
||||
disabled={unboundSelectionUnavailable}
|
||||
className="min-w-0 overflow-hidden py-2 pl-6 [&>span:last-child]:min-w-0 [&>span:last-child]:flex-1 [&>span:last-child]:overflow-hidden"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span aria-hidden className="h-4 w-4 shrink-0" />
|
||||
{unboundSelectionLoading ? (
|
||||
<Loader2
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 animate-spin text-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<span aria-hidden className="h-4 w-4 shrink-0" />
|
||||
)}
|
||||
<span className="shrink-0 text-sm font-medium leading-5">
|
||||
{noneOptionLabel ??
|
||||
t("codexOauth.useDefaultAccount", "使用默认账号")}
|
||||
@@ -317,6 +336,14 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
{noneOptionDescription}
|
||||
</span>
|
||||
)}
|
||||
{unboundSelectionLoading && (
|
||||
<span className="sr-only">
|
||||
{t(
|
||||
"codexOauth.nativeLoginAvailabilityLoading",
|
||||
"正在检查 Codex 登录是否可用...",
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
@@ -384,6 +411,34 @@ export const CodexOAuthSection: React.FC<CodexOAuthSectionProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unboundSelectionError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">
|
||||
{t(
|
||||
"codexOauth.nativeLoginAvailabilityLoadFailed",
|
||||
"无法检查 Codex 登录是否可用,请重试。",
|
||||
)}
|
||||
</span>
|
||||
{onUnboundSelectionRetry && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 shrink-0"
|
||||
disabled={unboundSelectionLoading}
|
||||
onClick={onUnboundSelectionRetry}
|
||||
>
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
{t("codexOauth.retry", "重试")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isStatusSuccess && !isStatusError && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -308,22 +308,40 @@ function ProviderFormFull({
|
||||
const isEditMode = Boolean(initialData);
|
||||
const isCodexNativeLoginProvider =
|
||||
appId === "codex" && providerId === CODEX_OFFICIAL_PROVIDER_ID;
|
||||
const shouldCheckCodexNativeLoginProvider =
|
||||
appId === "codex" && !isCodexNativeLoginProvider;
|
||||
const startsAsLegacyUnboundCodexOfficial =
|
||||
appId === "codex" &&
|
||||
isEditMode &&
|
||||
providerId !== CODEX_OFFICIAL_PROVIDER_ID &&
|
||||
initialData?.category === "official" &&
|
||||
!resolveManagedAccountId(initialData.meta, "codex_oauth")?.trim();
|
||||
const { data: codexNativeLoginProviderExists = true } = useQuery({
|
||||
const {
|
||||
data: codexNativeLoginProviderExists,
|
||||
isError: didCodexNativeLoginProviderFail,
|
||||
isFetching: isCodexNativeLoginProviderFetching,
|
||||
refetch: refetchCodexNativeLoginProvider,
|
||||
} = useQuery({
|
||||
queryKey: ["providers", "codex", "native-login-exists"],
|
||||
enabled: appId === "codex" && !isCodexNativeLoginProvider,
|
||||
enabled: shouldCheckCodexNativeLoginProvider,
|
||||
queryFn: async () => {
|
||||
const providers = await providersApi.getAll("codex");
|
||||
return Boolean(providers[CODEX_OFFICIAL_PROVIDER_ID]);
|
||||
},
|
||||
});
|
||||
const isCodexNativeLoginProviderLoading =
|
||||
shouldCheckCodexNativeLoginProvider && isCodexNativeLoginProviderFetching;
|
||||
const isCodexNativeLoginProviderError =
|
||||
shouldCheckCodexNativeLoginProvider &&
|
||||
didCodexNativeLoginProviderFail &&
|
||||
!isCodexNativeLoginProviderLoading;
|
||||
const isCodexNativeLoginProviderUnavailable =
|
||||
isCodexNativeLoginProviderLoading || isCodexNativeLoginProviderError;
|
||||
const isCodexNativeLoginProviderMissing =
|
||||
codexNativeLoginProviderExists === false &&
|
||||
!isCodexNativeLoginProviderUnavailable;
|
||||
const canCreateCodexNativeLoginProvider =
|
||||
appId === "codex" && !isEditMode && !codexNativeLoginProviderExists;
|
||||
appId === "codex" && !isEditMode && isCodexNativeLoginProviderMissing;
|
||||
const queryClient = useQueryClient();
|
||||
const { data: settingsData } = useSettingsQuery();
|
||||
const showCommonConfigNotice =
|
||||
@@ -823,9 +841,23 @@ function ProviderFormFull({
|
||||
const canSelectCodexNativeLogin =
|
||||
isCodexNativeLoginProvider ||
|
||||
canCreateCodexNativeLoginProvider ||
|
||||
(isEditMode && isCodexOfficialProvider && !codexNativeLoginProviderExists);
|
||||
(isEditMode &&
|
||||
isCodexOfficialProvider &&
|
||||
isCodexNativeLoginProviderMissing);
|
||||
const showCodexNativeLoginOption =
|
||||
canSelectCodexNativeLogin || isCodexNativeLoginProviderUnavailable;
|
||||
const requiresExplicitCodexOfficialSelection =
|
||||
isCodexOfficialProvider && !hasExplicitCodexOfficialSelection;
|
||||
isCodexOfficialProvider &&
|
||||
(!hasExplicitCodexOfficialSelection ||
|
||||
(!selectedCodexAccountId &&
|
||||
!canSelectCodexNativeLogin &&
|
||||
!isCodexNativeLoginProviderUnavailable));
|
||||
const hasUnconfirmedCodexNativeLoginSelection =
|
||||
isCodexOfficialProvider &&
|
||||
hasExplicitCodexOfficialSelection &&
|
||||
!selectedCodexAccountId &&
|
||||
!canSelectCodexNativeLogin &&
|
||||
isCodexNativeLoginProviderUnavailable;
|
||||
const requiresCodexOauthLogin =
|
||||
isClaudeCodexOauthProvider || isCodexOfficialManagedOauthBound;
|
||||
|
||||
@@ -1296,6 +1328,18 @@ function ProviderFormFull({
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (hasUnconfirmedCodexNativeLoginSelection) {
|
||||
toast.error(
|
||||
isCodexNativeLoginProviderError
|
||||
? t("codexOauth.nativeLoginAvailabilityLoadFailed", {
|
||||
defaultValue: "无法检查 Codex 登录是否可用,请重试。",
|
||||
})
|
||||
: t("codexOauth.nativeLoginAvailabilityLoading", {
|
||||
defaultValue: "正在检查 Codex 登录是否可用...",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requiresCodexOauthLogin && isCodexOauthStatusError) {
|
||||
toast.error(
|
||||
t("codexOauth.statusLoadFailed", {
|
||||
@@ -2482,10 +2526,17 @@ function ProviderFormFull({
|
||||
codexOauthNoneOptionDescription={t(
|
||||
"codex.followCodexLoginDescription",
|
||||
)}
|
||||
codexOauthAllowUnboundSelection={canSelectCodexNativeLogin}
|
||||
codexOauthAllowUnboundSelection={showCodexNativeLoginOption}
|
||||
codexOauthAllowUnboundSelectionWithoutStatus={
|
||||
canSelectCodexNativeLogin
|
||||
}
|
||||
codexOauthUnboundSelectionLoading={
|
||||
isCodexNativeLoginProviderLoading
|
||||
}
|
||||
codexOauthUnboundSelectionError={isCodexNativeLoginProviderError}
|
||||
onCodexOauthUnboundSelectionRetry={() =>
|
||||
void refetchCodexNativeLoginProvider()
|
||||
}
|
||||
codexOauthRequireExplicitSelection={
|
||||
requiresExplicitCodexOfficialSelection
|
||||
}
|
||||
|
||||
@@ -1410,6 +1410,8 @@
|
||||
"officialAccountPlaceholder": "Choose a sign-in method",
|
||||
"addOrManageAccounts": "Add or manage ChatGPT accounts…",
|
||||
"explicitSelectionRequired": "Choose a sign-in method",
|
||||
"nativeLoginAvailabilityLoading": "Checking Codex login availability...",
|
||||
"nativeLoginAvailabilityLoadFailed": "Could not check Codex login availability. Please retry.",
|
||||
"useDefaultAccount": "Use default account",
|
||||
"noneOptionLabel": "Follow Codex login",
|
||||
"reauthTitle": "Some accounts need to sign in again",
|
||||
|
||||
@@ -1410,6 +1410,8 @@
|
||||
"officialAccountPlaceholder": "ログイン方法を選択",
|
||||
"addOrManageAccounts": "ChatGPT アカウントを追加または管理…",
|
||||
"explicitSelectionRequired": "ログイン方法を選択してください",
|
||||
"nativeLoginAvailabilityLoading": "Codex ログインが利用可能か確認しています...",
|
||||
"nativeLoginAvailabilityLoadFailed": "Codex ログインが利用可能か確認できませんでした。再試行してください。",
|
||||
"useDefaultAccount": "デフォルトアカウントを使用",
|
||||
"noneOptionLabel": "Codex のログインに追従",
|
||||
"reauthTitle": "再ログインが必要なアカウントがあります",
|
||||
|
||||
@@ -1411,6 +1411,8 @@
|
||||
"officialAccountPlaceholder": "請選擇登入方式",
|
||||
"addOrManageAccounts": "新增或管理 ChatGPT 帳號…",
|
||||
"explicitSelectionRequired": "請先選擇登入方式",
|
||||
"nativeLoginAvailabilityLoading": "正在檢查 Codex 登入是否可用...",
|
||||
"nativeLoginAvailabilityLoadFailed": "無法檢查 Codex 登入是否可用,請重試。",
|
||||
"useDefaultAccount": "使用預設帳號",
|
||||
"noneOptionLabel": "跟隨 Codex 登入",
|
||||
"reauthTitle": "部分帳號需要重新登入",
|
||||
|
||||
@@ -1410,6 +1410,8 @@
|
||||
"officialAccountPlaceholder": "请选择登录方式",
|
||||
"addOrManageAccounts": "添加或管理 ChatGPT 账号…",
|
||||
"explicitSelectionRequired": "请先选择登录方式",
|
||||
"nativeLoginAvailabilityLoading": "正在检查 Codex 登录是否可用...",
|
||||
"nativeLoginAvailabilityLoadFailed": "无法检查 Codex 登录是否可用,请重试。",
|
||||
"useDefaultAccount": "使用默认账号",
|
||||
"noneOptionLabel": "跟随 Codex 登录",
|
||||
"reauthTitle": "部分账号需要重新登录",
|
||||
|
||||
@@ -254,6 +254,55 @@ describe("CodexOAuthSection", () => {
|
||||
expect(onAccountSelect).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it("shows the current-login option as disabled while availability loads", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CodexOAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={null}
|
||||
onAccountSelect={vi.fn()}
|
||||
noneOptionLabel="Follow Codex login"
|
||||
allowUnboundSelection
|
||||
unboundSelectionLoading
|
||||
requireExplicitSelection
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
const option = await screen.findByRole("option", {
|
||||
name: /Follow Codex login.*正在检查 Codex 登录是否可用/,
|
||||
});
|
||||
expect(option).toHaveAttribute("aria-disabled", "true");
|
||||
expect(option.querySelector(".animate-spin")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("explains an availability error and retries it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRetry = vi.fn();
|
||||
render(
|
||||
<CodexOAuthSection
|
||||
mode="select"
|
||||
selectedAccountId={null}
|
||||
onAccountSelect={vi.fn()}
|
||||
noneOptionLabel="Follow Codex login"
|
||||
allowUnboundSelection
|
||||
unboundSelectionError
|
||||
onUnboundSelectionRetry={onRetry}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"无法检查 Codex 登录是否可用,请重试。",
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "重试" }));
|
||||
expect(onRetry).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
expect(
|
||||
await screen.findByRole("option", { name: "Follow Codex login" }),
|
||||
).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("keeps an offline unbound choice available when account status fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
const authResult = mocks.useCodexOauth();
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -41,6 +47,9 @@ vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
|
||||
onSelectionConfirmed,
|
||||
onSelectionInvalidated,
|
||||
allowUnboundSelection = true,
|
||||
unboundSelectionLoading = false,
|
||||
unboundSelectionError = false,
|
||||
onUnboundSelectionRetry,
|
||||
nativeLoginOnly = false,
|
||||
requireExplicitSelection = false,
|
||||
}: {
|
||||
@@ -48,6 +57,9 @@ vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
|
||||
onSelectionConfirmed?: () => void;
|
||||
onSelectionInvalidated?: () => void;
|
||||
allowUnboundSelection?: boolean;
|
||||
unboundSelectionLoading?: boolean;
|
||||
unboundSelectionError?: boolean;
|
||||
onUnboundSelectionRetry?: () => void;
|
||||
nativeLoginOnly?: boolean;
|
||||
requireExplicitSelection?: boolean;
|
||||
}) => (
|
||||
@@ -58,6 +70,12 @@ vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
|
||||
<output data-testid="explicit-selection-required">
|
||||
{requireExplicitSelection ? "true" : "false"}
|
||||
</output>
|
||||
<output data-testid="native-login-loading">
|
||||
{unboundSelectionLoading ? "true" : "false"}
|
||||
</output>
|
||||
<output data-testid="native-login-error">
|
||||
{unboundSelectionError ? "true" : "false"}
|
||||
</output>
|
||||
<button
|
||||
type="button"
|
||||
disabled={nativeLoginOnly}
|
||||
@@ -71,6 +89,7 @@ vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
|
||||
{allowUnboundSelection && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={unboundSelectionLoading || unboundSelectionError}
|
||||
onClick={() => {
|
||||
onSelectionConfirmed?.();
|
||||
onAccountSelect?.(null);
|
||||
@@ -79,6 +98,14 @@ vi.mock("@/components/providers/forms/CodexOAuthSection", () => ({
|
||||
select-native-login
|
||||
</button>
|
||||
)}
|
||||
{unboundSelectionError && (
|
||||
<div role="alert">
|
||||
native-login-availability-error
|
||||
<button type="button" onClick={onUnboundSelectionRetry}>
|
||||
retry-native-login-lookup
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -173,8 +200,10 @@ vi.mock("@/lib/query", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
function renderCodexForm(onSubmit: (values: ProviderFormValues) => void) {
|
||||
const queryClient = createTestQueryClient();
|
||||
function renderCodexForm(
|
||||
onSubmit: (values: ProviderFormValues) => void,
|
||||
queryClient = createTestQueryClient(),
|
||||
) {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ProviderForm
|
||||
@@ -207,6 +236,14 @@ function renderClaudeCodexForm(onSubmit: (values: ProviderFormValues) => void) {
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForNativeLoginLookup() {
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("native-login-loading")).toHaveTextContent(
|
||||
"false",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe("ProviderForm Codex Official managed account", () => {
|
||||
beforeEach(() => {
|
||||
authState.codexReauthRequired = false;
|
||||
@@ -260,6 +297,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /OpenAI Official/ }));
|
||||
await screen.findByRole("button", { name: "select-managed-account" });
|
||||
await waitForNativeLoginLookup();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "select-native-login" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -277,6 +315,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
renderCodexForm(onSubmit);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /OpenAI Official/ }));
|
||||
await waitForNativeLoginLookup();
|
||||
const nativeLogin = await screen.findByRole("button", {
|
||||
name: "select-native-login",
|
||||
});
|
||||
@@ -289,6 +328,80 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
expect(submitted.meta?.authBinding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rechecks a native choice safely and offers retry on failure", async () => {
|
||||
let lookupState: "ready" | "pending" | "occupied" = "ready";
|
||||
let rejectPendingLookup!: (error: Error) => void;
|
||||
providerApiMocks.getAll.mockImplementation((appId: string) => {
|
||||
if (appId !== "codex" || lookupState === "ready") {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
if (lookupState === "occupied") {
|
||||
return Promise.resolve({ "codex-official": {} });
|
||||
}
|
||||
return new Promise((_, reject) => {
|
||||
rejectPendingLookup = reject;
|
||||
});
|
||||
});
|
||||
const queryClient = createTestQueryClient();
|
||||
const onSubmit = vi.fn();
|
||||
renderCodexForm(onSubmit, queryClient);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /OpenAI Official/ }));
|
||||
await waitForNativeLoginLookup();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "select-native-login" }),
|
||||
);
|
||||
|
||||
lookupState = "pending";
|
||||
act(() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["providers", "codex", "native-login-exists"],
|
||||
});
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("native-login-loading")).toHaveTextContent(
|
||||
"true",
|
||||
),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "select-native-login" }),
|
||||
).toBeDisabled();
|
||||
|
||||
act(() => rejectPendingLookup(new Error("refetch failed")));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("native-login-error")).toHaveTextContent(
|
||||
"true",
|
||||
),
|
||||
);
|
||||
expect(screen.getByTestId("explicit-selection-required")).toHaveTextContent(
|
||||
"false",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "select-native-login" }),
|
||||
).toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "save-provider" }));
|
||||
await waitFor(() =>
|
||||
expect(toastMocks.error).toHaveBeenCalledWith(
|
||||
"无法检查 Codex 登录是否可用,请重试。",
|
||||
),
|
||||
);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
lookupState = "occupied";
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "retry-native-login-lookup" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "select-native-login" }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.getByTestId("explicit-selection-required")).toHaveTextContent(
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not silently create the current-login card before a choice", async () => {
|
||||
providerApiMocks.getAll.mockResolvedValueOnce({});
|
||||
const onSubmit = vi.fn();
|
||||
@@ -333,8 +446,14 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows the fixed Official card to switch to a managed account", async () => {
|
||||
it("ignores cached lookup failures for the fixed Official card", async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
await expect(
|
||||
queryClient.fetchQuery({
|
||||
queryKey: ["providers", "codex", "native-login-exists"],
|
||||
queryFn: () => Promise.reject(new Error("cached lookup failure")),
|
||||
}),
|
||||
).rejects.toThrow("cached lookup failure");
|
||||
const onSubmit = vi.fn();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -354,6 +473,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("native-login-only")).toHaveTextContent("false");
|
||||
expect(screen.getByTestId("native-login-error")).toHaveTextContent("false");
|
||||
expect(
|
||||
screen.getByRole("button", { name: "select-managed-account" }),
|
||||
).toBeEnabled();
|
||||
@@ -411,7 +531,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not offer the current-login option on a managed Official card", () => {
|
||||
it("does not offer the current-login option on a managed Official card", async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -439,6 +559,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("native-login-only")).toHaveTextContent("false");
|
||||
await waitForNativeLoginLookup();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "select-native-login" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -473,6 +594,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitForNativeLoginLookup();
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: "select-native-login" }),
|
||||
);
|
||||
@@ -510,6 +632,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("native-login-only")).toHaveTextContent("false");
|
||||
await waitForNativeLoginLookup();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "select-native-login" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -553,6 +676,7 @@ describe("ProviderForm Codex Official managed account", () => {
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
await waitForNativeLoginLookup();
|
||||
const nativeLogin = await screen.findByRole("button", {
|
||||
name: "select-native-login",
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user