mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:08:02 +08:00
test(monitor-ui): 配额视图渲染与开关门控用例
- 新增 MonitorQuotaView.spec:tier window/label 的 i18n 映射与未知 token 透出、阈值配色(≥90 红/≥75 黄/其余绿)、进度条宽度钳制、 多币种余额与单余额回退、plan_level 徽章、错误截断与失败态不渲染 tiers - 新增 MonitorCard.quota.spec:latest_quota 受系统开关门控, 无快照或开关关闭时不渲染 - 既有工厂/断言补 check_mode、account_id;provider 网格断言 4→8
This commit is contained in:
@@ -34,6 +34,8 @@ function makeMonitor(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
|
||||
extra_headers: {},
|
||||
body_override_mode: 'off',
|
||||
body_override: null,
|
||||
check_mode: 'probe',
|
||||
account_id: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { MonitorQuotaSnapshot } from '@/api/admin/channelMonitor'
|
||||
import MonitorQuotaView from '../MonitorQuotaView.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
// te() 恒真:已知 token 直接返回 i18n key,便于断言 window/label 映射。
|
||||
useI18n: () => ({ t: (key: string) => key, te: () => true }),
|
||||
}
|
||||
})
|
||||
|
||||
function makeSnapshot(overrides: Partial<MonitorQuotaSnapshot> = {}): MonitorQuotaSnapshot {
|
||||
return {
|
||||
source: 'usage',
|
||||
success: true,
|
||||
fetched_at: '2026-08-18T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('MonitorQuotaView', () => {
|
||||
it('renders nothing without a snapshot', () => {
|
||||
const wrapper = mount(MonitorQuotaView, { props: { snapshot: null } })
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(false)
|
||||
expect(wrapper.text()).toBe('')
|
||||
})
|
||||
|
||||
it('maps tier window/label tokens through i18n and colors utilization', () => {
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: {
|
||||
snapshot: makeSnapshot({
|
||||
tiers: [
|
||||
{ window: '5h', used_percent: 42.4 },
|
||||
{ window: '7d', label: 'pro', used_percent: 80 },
|
||||
{ window: 'weekly', label: 'unknown-token', used_percent: 95 },
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const rows = wrapper.findAll('[data-testid="monitor-quota-view"] .flex.items-center')
|
||||
// tier 行只在 success 且有数据时渲染
|
||||
expect(rows.length).toBeGreaterThanOrEqual(3)
|
||||
const text = wrapper.text()
|
||||
// 已知 window token 走 i18n
|
||||
expect(text).toContain('monitorCommon.quota.windows.5h')
|
||||
expect(text).toContain('monitorCommon.quota.windows.7d')
|
||||
// 已知 label token 拼成 label/window
|
||||
expect(text).toContain('monitorCommon.quota.labels.pro/monitorCommon.quota.windows.7d')
|
||||
// 未知 label 原样透出(前向兼容)
|
||||
expect(text).toContain('unknown-token/monitorCommon.quota.windows.weekly')
|
||||
// 百分比取整
|
||||
expect(text).toContain('42%')
|
||||
expect(text).toContain('80%')
|
||||
expect(text).toContain('95%')
|
||||
|
||||
const html = wrapper.html()
|
||||
// 阈值配色:≥90 红 / ≥75 黄 / 其余绿(与账号页 CNProviderQuotaCell 对齐)
|
||||
expect(html).toContain('bg-emerald-500')
|
||||
expect(html).toContain('bg-amber-500')
|
||||
expect(html).toContain('bg-red-500')
|
||||
})
|
||||
|
||||
it('clamps the tier bar width into 0-100', () => {
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: {
|
||||
snapshot: makeSnapshot({ tiers: [{ window: '5h', used_percent: 240 }] }),
|
||||
},
|
||||
})
|
||||
expect(wrapper.html()).toContain('width: 100%')
|
||||
})
|
||||
|
||||
it('shows the plan level badge and multi-currency balances', () => {
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: {
|
||||
snapshot: makeSnapshot({
|
||||
plan_level: 'Max20',
|
||||
balances: [
|
||||
{ currency: 'CNY', balance: 12.5 },
|
||||
{ currency: 'USD', balance: 0 },
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Max20')
|
||||
expect(wrapper.text()).toContain('12.50 CNY')
|
||||
expect(wrapper.text()).toContain('0.00 USD')
|
||||
// 余额为 0 用红色警示
|
||||
expect(wrapper.html()).toContain('text-red-600')
|
||||
})
|
||||
|
||||
it('falls back to the single balance + currency pair', () => {
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: { snapshot: makeSnapshot({ balance: 3.2, currency: 'CNY' }) },
|
||||
})
|
||||
expect(wrapper.text()).toContain('3.20 CNY')
|
||||
})
|
||||
|
||||
it('renders a truncated error state when the fetch failed', () => {
|
||||
const longError = 'x'.repeat(60)
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: {
|
||||
snapshot: makeSnapshot({ success: false, error: longError }),
|
||||
},
|
||||
})
|
||||
|
||||
const error = wrapper.get('[data-testid="monitor-quota-error"]')
|
||||
expect(error.text()).toBe(`${'x'.repeat(48)}…`)
|
||||
expect(error.attributes('title')).toBe(longError)
|
||||
})
|
||||
|
||||
it('keeps failed snapshots from rendering tier rows', () => {
|
||||
const wrapper = mount(MonitorQuotaView, {
|
||||
props: {
|
||||
snapshot: makeSnapshot({
|
||||
success: false,
|
||||
tiers: [{ window: '5h', used_percent: 10 }],
|
||||
}),
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('10%')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { UserMonitorView } from '@/api/channelMonitor'
|
||||
import MonitorCard from '../monitor/MonitorCard.vue'
|
||||
|
||||
const { isQuotaVisible } = vi.hoisted(() => ({
|
||||
isQuotaVisible: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/featureFlags', () => ({
|
||||
isChannelMonitorQuotaVisible: () => isQuotaVisible(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key, te: () => true }),
|
||||
}
|
||||
})
|
||||
|
||||
function makeItem(overrides: Partial<UserMonitorView> = {}): UserMonitorView {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'claude-main',
|
||||
provider: 'kimi',
|
||||
group_name: '',
|
||||
primary_model: 'quota',
|
||||
primary_status: 'operational',
|
||||
primary_latency_ms: null,
|
||||
primary_ping_latency_ms: null,
|
||||
availability_7d: 100,
|
||||
extra_models: [],
|
||||
timeline: [],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function mountCard(item: UserMonitorView) {
|
||||
return mount(MonitorCard, {
|
||||
props: {
|
||||
item,
|
||||
window: '7d',
|
||||
availabilityValue: 100,
|
||||
countdownSeconds: 0,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
MonitorMetricPair: true,
|
||||
MonitorAvailabilityRow: true,
|
||||
MonitorTimeline: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('MonitorCard quota snapshot visibility', () => {
|
||||
it('hides the quota block when the system switch is off even if data exists', () => {
|
||||
isQuotaVisible.mockReturnValue(false)
|
||||
const wrapper = mountCard(
|
||||
makeItem({
|
||||
latest_quota: { source: 'cn_quota', success: true, fetched_at: '2026-08-18T00:00:00Z' },
|
||||
}),
|
||||
)
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders the quota block when the switch is on and a snapshot exists', () => {
|
||||
isQuotaVisible.mockReturnValue(true)
|
||||
const wrapper = mountCard(
|
||||
makeItem({
|
||||
latest_quota: {
|
||||
source: 'cn_quota',
|
||||
success: true,
|
||||
plan_level: 'kimi-plus',
|
||||
tiers: [{ window: 'daily', label: 'requests', used_percent: 60 }],
|
||||
fetched_at: '2026-08-18T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('kimi-plus')
|
||||
})
|
||||
|
||||
it('never renders the quota block without a snapshot', () => {
|
||||
isQuotaVisible.mockReturnValue(true)
|
||||
const wrapper = mountCard(makeItem())
|
||||
expect(wrapper.find('[data-testid="monitor-quota-view"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -96,6 +96,8 @@ function makeMonitor(overrides: Partial<ChannelMonitor> = {}): ChannelMonitor {
|
||||
extra_headers: {},
|
||||
body_override_mode: 'off',
|
||||
body_override: null,
|
||||
check_mode: 'probe',
|
||||
account_id: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('channel monitor Grok provider', () => {
|
||||
|
||||
expect(PROVIDERS).toContain(PROVIDER_GROK)
|
||||
const providerButtons = wrapper.findAll('[data-testid^="monitor-provider-"]')
|
||||
expect(providerButtons).toHaveLength(4)
|
||||
expect(providerButtons).toHaveLength(8)
|
||||
expect(providerButtons[0].element.parentElement?.className).toContain('grid-cols-2')
|
||||
expect(providerButtons[0].element.parentElement?.className).toContain('sm:grid-cols-4')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user