mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
refactor(frontend): extract provider key identity block
This commit is contained in:
@@ -112,137 +112,30 @@
|
||||
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
class="text-sm font-medium truncate"
|
||||
:class="key.name ? 'cursor-pointer hover:text-primary transition-colors' : ''"
|
||||
:title="key.name ? legacyT('点击复制') : ''"
|
||||
@click.stop="key.name && copyToClipboard(key.name)"
|
||||
>{{ key.name || legacyT('未命名密钥') }}</span>
|
||||
<!-- OAuth 订阅类型标签 (Codex) -->
|
||||
<Badge
|
||||
v-if="key.oauth_plan_type"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:class="getOAuthPlanTypeClass(key.oauth_plan_type)"
|
||||
>
|
||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getOAuthOrgBadge(key)"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getOAuthOrgBadge(key)?.title"
|
||||
>
|
||||
{{ getOAuthOrgBadge(key)?.label }}
|
||||
</Badge>
|
||||
<!-- Kiro 订阅类型标签 -->
|
||||
<Badge
|
||||
v-if="shouldShowKiroSubscriptionBadge(key)"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:class="getOAuthPlanTypeClass(getKiroSubscriptionBadgeLabel(key))"
|
||||
>
|
||||
{{ getKiroSubscriptionBadgeLabel(key) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ getProviderMaskedSecretLabel(key, provider.provider_type) }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="canExportOAuthCredential(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:title="legacyT('下载 OAuth 授权文件')"
|
||||
@click.stop="downloadRefreshToken(key)"
|
||||
>
|
||||
<Download class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:title="legacyT('复制密钥')"
|
||||
@click.stop="copyFullKey(key)"
|
||||
>
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<!-- OAuth 状态(失效/过期/倒计时)和刷新按钮 -->
|
||||
<template v-if="shouldShowOAuthRefreshControl(key, provider.provider_type)">
|
||||
<!-- 账号级别异常:醒目提示 + 清除按钮 -->
|
||||
<template v-if="isAccountLevelBlock(key)">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0 gap-0.5"
|
||||
:title="getOAuthStatusTitle(key)"
|
||||
>
|
||||
<ShieldX class="w-2.5 h-2.5" />
|
||||
{{ legacyT('账号异常') }}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0 text-destructive hover:text-destructive"
|
||||
:disabled="clearingOAuthInvalidKeyId === key.id"
|
||||
:title="legacyT('清除异常标记(确认账号已完成验证后使用)')"
|
||||
@click.stop="handleClearOAuthInvalid(key)"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-2.5 h-2.5"
|
||||
:class="{ 'animate-spin': clearingOAuthInvalidKeyId === key.id }"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<!-- 普通 OAuth 状态 -->
|
||||
<template v-else>
|
||||
<span
|
||||
class="text-[10px]"
|
||||
:class="{
|
||||
'text-destructive': getKeyOAuthExpires(key)?.isInvalid || getKeyOAuthExpires(key)?.isExpired,
|
||||
'text-warning': getKeyOAuthExpires(key)?.isExpiringSoon && !getKeyOAuthExpires(key)?.isExpired && !getKeyOAuthExpires(key)?.isInvalid,
|
||||
'text-muted-foreground': !getKeyOAuthExpires(key)?.isExpired && !getKeyOAuthExpires(key)?.isExpiringSoon && !getKeyOAuthExpires(key)?.isInvalid
|
||||
}"
|
||||
:title="getOAuthStatusTitle(key)"
|
||||
>
|
||||
{{ getKeyOAuthExpires(key)?.text }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="key.oauth_temporary"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:title="legacyT('仅通过 Access Token 导入,无法自动刷新,到期后需要重新导入')"
|
||||
>
|
||||
{{ legacyT('临时') }}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:disabled="refreshingOAuthKeyId === key.id || !canRefreshOAuthCredential(key)"
|
||||
:title="getOAuthRefreshButtonTitle(key)"
|
||||
@click.stop="handleRefreshOAuth(key)"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-2.5 h-2.5"
|
||||
:class="{ 'animate-spin': refreshingOAuthKeyId === key.id }"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
<!-- Antigravity 账号未激活提示 -->
|
||||
<span
|
||||
v-if="provider.provider_type === 'antigravity' && key.is_active && isOAuthManagedCredential(key) && !hasAntigravityQuotaDisplayData(key)"
|
||||
class="text-[10px] text-orange-500 dark:text-orange-400"
|
||||
:title="legacyT('该账号尚未完成 Gemini Code Assist 激活,无法获取配额和使用模型')"
|
||||
>
|
||||
{{ legacyT('账号未激活') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderKeyIdentityBlock
|
||||
:api-key="key"
|
||||
:masked-secret-label="getProviderMaskedSecretLabel(key, provider.provider_type)"
|
||||
:oauth-plan-label="key.oauth_plan_type ? formatOAuthPlanType(key.oauth_plan_type) : null"
|
||||
:oauth-plan-class="key.oauth_plan_type ? getOAuthPlanTypeClass(key.oauth_plan_type) : ''"
|
||||
:oauth-org-badge="getOAuthOrgBadge(key)"
|
||||
:kiro-subscription-label="shouldShowKiroSubscriptionBadge(key) ? getKiroSubscriptionBadgeLabel(key) : null"
|
||||
:kiro-subscription-class="shouldShowKiroSubscriptionBadge(key) ? getOAuthPlanTypeClass(getKiroSubscriptionBadgeLabel(key)) : ''"
|
||||
:can-export-credential="canExportOAuthCredential(key)"
|
||||
:show-o-auth-refresh-control="shouldShowOAuthRefreshControl(key, provider.provider_type)"
|
||||
:account-level-block="isAccountLevelBlock(key)"
|
||||
:oauth-status="getKeyOAuthExpires(key)"
|
||||
:oauth-status-title="getOAuthStatusTitle(key)"
|
||||
:oauth-refresh-button-title="getOAuthRefreshButtonTitle(key)"
|
||||
:can-refresh-credential="canRefreshOAuthCredential(key)"
|
||||
:clearing-o-auth-invalid="clearingOAuthInvalidKeyId === key.id"
|
||||
:refreshing-o-auth="refreshingOAuthKeyId === key.id"
|
||||
:antigravity-inactive="provider.provider_type === 'antigravity' && key.is_active && isOAuthManagedCredential(key) && !hasAntigravityQuotaDisplayData(key)"
|
||||
@copy-name="copyToClipboard"
|
||||
@download-credential="downloadRefreshToken(key)"
|
||||
@copy-full-key="copyFullKey(key)"
|
||||
@clear-o-auth-invalid="handleClearOAuthInvalid(key)"
|
||||
@refresh-o-auth="handleRefreshOAuth(key)"
|
||||
/>
|
||||
</div>
|
||||
<ProviderKeyActionCluster
|
||||
:api-key="key"
|
||||
@@ -967,17 +860,13 @@ import {
|
||||
Plus,
|
||||
Key,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
GripVertical,
|
||||
Copy,
|
||||
Download,
|
||||
ShieldX,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import { useI18n } from '@/i18n'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
@@ -1009,6 +898,7 @@ import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQ
|
||||
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
|
||||
import ProviderDetailHeader from '@/features/providers/components/ProviderDetailHeader.vue'
|
||||
import ProviderKeyActionCluster from '@/features/providers/components/ProviderKeyActionCluster.vue'
|
||||
import ProviderKeyIdentityBlock from '@/features/providers/components/ProviderKeyIdentityBlock.vue'
|
||||
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
class="text-sm font-medium truncate"
|
||||
:class="apiKey.name ? 'cursor-pointer hover:text-primary transition-colors' : ''"
|
||||
:title="apiKey.name ? legacyT('点击复制') : ''"
|
||||
data-testid="provider-key-name"
|
||||
@click.stop="apiKey.name && $emit('copyName', apiKey.name)"
|
||||
>
|
||||
{{ apiKey.name || legacyT('未命名密钥') }}
|
||||
</span>
|
||||
|
||||
<Badge
|
||||
v-if="oauthPlanLabel"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:class="oauthPlanClass"
|
||||
data-testid="provider-key-oauth-plan"
|
||||
>
|
||||
{{ oauthPlanLabel }}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
v-if="oauthOrgBadge"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="oauthOrgBadge.title"
|
||||
data-testid="provider-key-oauth-org"
|
||||
>
|
||||
{{ oauthOrgBadge.label }}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
v-if="kiroSubscriptionLabel"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:class="kiroSubscriptionClass"
|
||||
data-testid="provider-key-kiro-plan"
|
||||
>
|
||||
{{ kiroSubscriptionLabel }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ maskedSecretLabel }}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
v-if="canExportCredential"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:title="legacyT('下载 OAuth 授权文件')"
|
||||
@click.stop="$emit('downloadCredential')"
|
||||
>
|
||||
<Download class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:title="legacyT('复制密钥')"
|
||||
@click.stop="$emit('copyFullKey')"
|
||||
>
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
|
||||
<template v-if="showOAuthRefreshControl">
|
||||
<template v-if="accountLevelBlock">
|
||||
<Badge
|
||||
variant="destructive"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0 gap-0.5"
|
||||
:title="oauthStatusTitle"
|
||||
data-testid="provider-key-account-block"
|
||||
>
|
||||
<ShieldX class="w-2.5 h-2.5" />
|
||||
{{ legacyT('账号异常') }}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0 text-destructive hover:text-destructive"
|
||||
:disabled="clearingOAuthInvalid"
|
||||
:title="legacyT('清除异常标记(确认账号已完成验证后使用)')"
|
||||
@click.stop="$emit('clearOAuthInvalid')"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-2.5 h-2.5"
|
||||
:class="{ 'animate-spin': clearingOAuthInvalid }"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span
|
||||
class="text-[10px]"
|
||||
:class="oauthStatusClass"
|
||||
:title="oauthStatusTitle"
|
||||
data-testid="provider-key-oauth-status"
|
||||
>
|
||||
{{ oauthStatus?.text }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="apiKey.oauth_temporary"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
:title="legacyT('仅通过 Access Token 导入,无法自动刷新,到期后需要重新导入')"
|
||||
data-testid="provider-key-temporary"
|
||||
>
|
||||
{{ legacyT('临时') }}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:disabled="refreshingOAuth || !canRefreshCredential"
|
||||
:title="oauthRefreshButtonTitle"
|
||||
@click.stop="$emit('refreshOAuth')"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-2.5 h-2.5"
|
||||
:class="{ 'animate-spin': refreshingOAuth }"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<span
|
||||
v-if="antigravityInactive"
|
||||
class="text-[10px] text-orange-500 dark:text-orange-400"
|
||||
:title="legacyT('该账号尚未完成 Gemini Code Assist 激活,无法获取配额和使用模型')"
|
||||
data-testid="provider-key-antigravity-inactive"
|
||||
>
|
||||
{{ legacyT('账号未激活') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Copy, Download, RefreshCw, ShieldX } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
import type { EndpointAPIKey } from '@/api/endpoints'
|
||||
import type { OAuthStatusInfo } from '@/composables/useCountdownTimer'
|
||||
|
||||
interface OAuthOrgBadgeDisplay {
|
||||
label: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
apiKey: EndpointAPIKey
|
||||
maskedSecretLabel: string
|
||||
oauthPlanLabel?: string | null
|
||||
oauthPlanClass?: string
|
||||
oauthOrgBadge?: OAuthOrgBadgeDisplay | null
|
||||
kiroSubscriptionLabel?: string | null
|
||||
kiroSubscriptionClass?: string
|
||||
canExportCredential?: boolean
|
||||
showOAuthRefreshControl?: boolean
|
||||
accountLevelBlock?: boolean
|
||||
oauthStatus?: OAuthStatusInfo | null
|
||||
oauthStatusTitle?: string
|
||||
oauthRefreshButtonTitle?: string
|
||||
canRefreshCredential?: boolean
|
||||
clearingOAuthInvalid?: boolean
|
||||
refreshingOAuth?: boolean
|
||||
antigravityInactive?: boolean
|
||||
}>(), {
|
||||
oauthPlanLabel: null,
|
||||
oauthPlanClass: '',
|
||||
oauthOrgBadge: null,
|
||||
kiroSubscriptionLabel: null,
|
||||
kiroSubscriptionClass: '',
|
||||
canExportCredential: false,
|
||||
showOAuthRefreshControl: false,
|
||||
accountLevelBlock: false,
|
||||
oauthStatus: null,
|
||||
oauthStatusTitle: '',
|
||||
oauthRefreshButtonTitle: '',
|
||||
canRefreshCredential: false,
|
||||
clearingOAuthInvalid: false,
|
||||
refreshingOAuth: false,
|
||||
antigravityInactive: false,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'copyName', name: string): void
|
||||
(e: 'downloadCredential'): void
|
||||
(e: 'copyFullKey'): void
|
||||
(e: 'clearOAuthInvalid'): void
|
||||
(e: 'refreshOAuth'): void
|
||||
}>()
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const oauthStatusClass = computed(() => {
|
||||
const status = props.oauthStatus
|
||||
return {
|
||||
'text-destructive': status?.isInvalid || status?.isExpired,
|
||||
'text-warning': status?.isExpiringSoon && !status?.isExpired && !status?.isInvalid,
|
||||
'text-muted-foreground': !status?.isExpired && !status?.isExpiringSoon && !status?.isInvalid,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import ProviderKeyIdentityBlock from '@/features/providers/components/ProviderKeyIdentityBlock.vue'
|
||||
import type { EndpointAPIKey } from '@/api/endpoints'
|
||||
import { createI18n } from '@/i18n'
|
||||
|
||||
vi.mock('lucide-vue-next', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
const Icon = defineComponent({
|
||||
name: 'IconStub',
|
||||
setup() {
|
||||
return () => h('span')
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Copy: Icon,
|
||||
Download: Icon,
|
||||
RefreshCw: Icon,
|
||||
ShieldX: Icon,
|
||||
}
|
||||
})
|
||||
|
||||
function createProviderKey(overrides: Partial<EndpointAPIKey> = {}): EndpointAPIKey {
|
||||
return {
|
||||
id: 'provider-key-1',
|
||||
provider_id: 'provider-1',
|
||||
api_formats: ['openai:chat'],
|
||||
api_key_masked: 'sk-***',
|
||||
auth_type: 'oauth',
|
||||
name: 'Primary key',
|
||||
internal_priority: 10,
|
||||
cache_ttl_minutes: 0,
|
||||
max_probe_interval_minutes: 5,
|
||||
health_score: 1,
|
||||
consecutive_failures: 0,
|
||||
request_count: 0,
|
||||
success_count: 0,
|
||||
error_count: 0,
|
||||
success_rate: 1,
|
||||
avg_response_time_ms: 0,
|
||||
is_active: true,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mount(props: Record<string, unknown>) {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(ProviderKeyIdentityBlock, props)
|
||||
},
|
||||
}))
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
return {
|
||||
root,
|
||||
unmount: () => {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('ProviderKeyIdentityBlock', () => {
|
||||
it('renders identity badges and emits copy/download/refresh actions', () => {
|
||||
const onCopyName = vi.fn()
|
||||
const onDownloadCredential = vi.fn()
|
||||
const onRefreshOAuth = vi.fn()
|
||||
|
||||
const { root, unmount } = mount({
|
||||
apiKey: createProviderKey({
|
||||
oauth_temporary: true,
|
||||
}),
|
||||
maskedSecretLabel: 'oauth:***',
|
||||
oauthPlanLabel: 'Team',
|
||||
oauthPlanClass: 'text-purple-600',
|
||||
oauthOrgBadge: { label: 'org:demo', title: 'org_id: demo' },
|
||||
kiroSubscriptionLabel: 'Pro',
|
||||
kiroSubscriptionClass: 'text-blue-600',
|
||||
canExportCredential: true,
|
||||
showOAuthRefreshControl: true,
|
||||
oauthStatus: { text: '12m', isExpired: false, isExpiringSoon: true, isInvalid: false },
|
||||
oauthStatusTitle: 'OAuth expires soon',
|
||||
oauthRefreshButtonTitle: 'Refresh OAuth',
|
||||
canRefreshCredential: true,
|
||||
onCopyName,
|
||||
onDownloadCredential,
|
||||
onRefreshOAuth,
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-key-name"]')?.textContent).toContain('Primary key')
|
||||
expect(root.querySelector('[data-testid="provider-key-oauth-plan"]')?.textContent).toContain('Team')
|
||||
expect(root.querySelector('[data-testid="provider-key-oauth-org"]')?.textContent).toContain('org:demo')
|
||||
expect(root.querySelector('[data-testid="provider-key-kiro-plan"]')?.textContent).toContain('Pro')
|
||||
expect(root.querySelector('[data-testid="provider-key-oauth-status"]')?.textContent).toContain('12m')
|
||||
expect(root.querySelector('[data-testid="provider-key-temporary"]')).toBeTruthy()
|
||||
|
||||
;(root.querySelector('[data-testid="provider-key-name"]') as HTMLElement).click()
|
||||
;(root.querySelector('button[title="下载 OAuth 授权文件"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="Refresh OAuth"]') as HTMLButtonElement).click()
|
||||
|
||||
expect(onCopyName).toHaveBeenCalledWith('Primary key')
|
||||
expect(onDownloadCredential).toHaveBeenCalledTimes(1)
|
||||
expect(onRefreshOAuth).toHaveBeenCalledTimes(1)
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('renders account block and inactive account states', () => {
|
||||
const onCopyFullKey = vi.fn()
|
||||
const onClearOAuthInvalid = vi.fn()
|
||||
|
||||
const { root, unmount } = mount({
|
||||
apiKey: createProviderKey({ name: '' }),
|
||||
maskedSecretLabel: 'sk-***',
|
||||
canExportCredential: false,
|
||||
showOAuthRefreshControl: true,
|
||||
accountLevelBlock: true,
|
||||
oauthStatusTitle: 'Account blocked',
|
||||
antigravityInactive: true,
|
||||
onCopyFullKey,
|
||||
onClearOAuthInvalid,
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-key-name"]')?.textContent).toContain('未命名密钥')
|
||||
expect(root.querySelector('[data-testid="provider-key-account-block"]')?.textContent).toContain('账号异常')
|
||||
expect(root.querySelector('[data-testid="provider-key-antigravity-inactive"]')?.textContent).toContain('账号未激活')
|
||||
|
||||
;(root.querySelector('button[title="复制密钥"]') as HTMLButtonElement).click()
|
||||
;(root.querySelector('button[title="清除异常标记(确认账号已完成验证后使用)"]') as HTMLButtonElement).click()
|
||||
|
||||
expect(onCopyFullKey).toHaveBeenCalledTimes(1)
|
||||
expect(onClearOAuthInvalid).toHaveBeenCalledTimes(1)
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as ProviderFormDialog } from './ProviderFormDialog.vue'
|
||||
export { default as ProviderKeyActionCluster } from './ProviderKeyActionCluster.vue'
|
||||
export { default as ProviderKeyIdentityBlock } from './ProviderKeyIdentityBlock.vue'
|
||||
export { default as ProviderMonthlyQuotaCard } from './ProviderMonthlyQuotaCard.vue'
|
||||
export { default as ProviderQuotaProgressRow } from './ProviderQuotaProgressRow.vue'
|
||||
export { default as ProviderQuotaSectionHeader } from './ProviderQuotaSectionHeader.vue'
|
||||
|
||||
Reference in New Issue
Block a user