| import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' |
| import { flushPromises, mount } from '@vue/test-utils' |
|
|
| import UsageView from '../UsageView.vue' |
|
|
| const { list, getStats, getSnapshotV2, getById } = vi.hoisted(() => { |
| vi.stubGlobal('localStorage', { |
| getItem: vi.fn(() => null), |
| setItem: vi.fn(), |
| removeItem: vi.fn(), |
| }) |
|
|
| return { |
| list: vi.fn(), |
| getStats: vi.fn(), |
| getSnapshotV2: vi.fn(), |
| getById: vi.fn(), |
| } |
| }) |
|
|
| const messages: Record<string, string> = { |
| 'admin.dashboard.timeRange': 'Time Range', |
| 'admin.dashboard.day': 'Day', |
| 'admin.dashboard.hour': 'Hour', |
| 'admin.usage.failedToLoadUser': 'Failed to load user', |
| } |
|
|
| const formatLocalDate = (date: Date): string => { |
| const year = date.getFullYear() |
| const month = String(date.getMonth() + 1).padStart(2, '0') |
| const day = String(date.getDate()).padStart(2, '0') |
| return `${year}-${month}-${day}` |
| } |
|
|
| vi.mock('@/api/admin', () => ({ |
| adminAPI: { |
| usage: { |
| list, |
| getStats, |
| }, |
| dashboard: { |
| getSnapshotV2, |
| }, |
| users: { |
| getById, |
| }, |
| }, |
| })) |
|
|
| vi.mock('@/api/admin/usage', () => ({ |
| adminUsageAPI: { |
| list: vi.fn(), |
| }, |
| })) |
|
|
| vi.mock('@/stores/app', () => ({ |
| useAppStore: () => ({ |
| showError: vi.fn(), |
| showWarning: vi.fn(), |
| showSuccess: vi.fn(), |
| showInfo: vi.fn(), |
| }), |
| })) |
|
|
| vi.mock('@/utils/format', () => ({ |
| formatReasoningEffort: (value: string | null | undefined) => value ?? '-', |
| })) |
|
|
| vi.mock('vue-i18n', async () => { |
| const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n') |
| return { |
| ...actual, |
| useI18n: () => ({ |
| t: (key: string) => messages[key] ?? key, |
| }), |
| } |
| }) |
|
|
| vi.mock('vue-router', () => ({ |
| useRoute: () => ({ |
| query: {} |
| }) |
| })) |
|
|
| const AppLayoutStub = { template: '<div><slot /></div>' } |
| const UsageFiltersStub = { template: '<div><slot name="after-reset" /></div>' } |
| const ModelDistributionChartStub = { |
| props: ['metric'], |
| emits: ['update:metric'], |
| template: ` |
| <div data-test="model-chart"> |
| <span class="metric">{{ metric }}</span> |
| <button class="switch-metric" @click="$emit('update:metric', 'actual_cost')">switch</button> |
| </div> |
| `, |
| } |
| const GroupDistributionChartStub = { |
| props: ['metric'], |
| emits: ['update:metric'], |
| template: ` |
| <div data-test="group-chart"> |
| <span class="metric">{{ metric }}</span> |
| <button class="switch-metric" @click="$emit('update:metric', 'actual_cost')">switch</button> |
| </div> |
| `, |
| } |
|
|
| describe('admin UsageView distribution metric toggles', () => { |
| beforeEach(() => { |
| vi.useFakeTimers() |
| list.mockReset() |
| getStats.mockReset() |
| getSnapshotV2.mockReset() |
| getById.mockReset() |
|
|
| list.mockResolvedValue({ |
| items: [], |
| total: 0, |
| pages: 0, |
| }) |
| getStats.mockResolvedValue({ |
| total_requests: 0, |
| total_input_tokens: 0, |
| total_output_tokens: 0, |
| total_cache_tokens: 0, |
| total_tokens: 0, |
| total_cost: 0, |
| total_actual_cost: 0, |
| average_duration_ms: 0, |
| }) |
| getSnapshotV2.mockResolvedValue({ |
| trend: [], |
| models: [], |
| groups: [], |
| }) |
| }) |
|
|
| afterEach(() => { |
| vi.useRealTimers() |
| }) |
|
|
| it('keeps model and group metric toggles independent without refetching chart data', async () => { |
| const wrapper = mount(UsageView, { |
| global: { |
| stubs: { |
| AppLayout: AppLayoutStub, |
| UsageStatsCards: true, |
| UsageFilters: UsageFiltersStub, |
| UsageTable: true, |
| UsageExportProgress: true, |
| UsageCleanupDialog: true, |
| UserBalanceHistoryModal: true, |
| Pagination: true, |
| Select: true, |
| DateRangePicker: true, |
| Icon: true, |
| TokenUsageTrend: true, |
| ModelDistributionChart: ModelDistributionChartStub, |
| GroupDistributionChart: GroupDistributionChartStub, |
| }, |
| }, |
| }) |
|
|
| vi.advanceTimersByTime(120) |
| await flushPromises() |
|
|
| expect(getSnapshotV2).toHaveBeenCalledTimes(1) |
| const now = new Date() |
| const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
| expect(getSnapshotV2).toHaveBeenCalledWith(expect.objectContaining({ |
| start_date: formatLocalDate(yesterday), |
| end_date: formatLocalDate(now), |
| granularity: 'hour' |
| })) |
|
|
| const modelChart = wrapper.find('[data-test="model-chart"]') |
| const groupChart = wrapper.find('[data-test="group-chart"]') |
|
|
| expect(modelChart.find('.metric').text()).toBe('tokens') |
| expect(groupChart.find('.metric').text()).toBe('tokens') |
|
|
| await modelChart.find('.switch-metric').trigger('click') |
| await flushPromises() |
|
|
| expect(modelChart.find('.metric').text()).toBe('actual_cost') |
| expect(groupChart.find('.metric').text()).toBe('tokens') |
| expect(getSnapshotV2).toHaveBeenCalledTimes(1) |
|
|
| await groupChart.find('.switch-metric').trigger('click') |
| await flushPromises() |
|
|
| expect(modelChart.find('.metric').text()).toBe('actual_cost') |
| expect(groupChart.find('.metric').text()).toBe('actual_cost') |
| expect(getSnapshotV2).toHaveBeenCalledTimes(1) |
| }) |
| }) |
|
|