import { describe, expect, it, vi, beforeEach } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import { nextTick } from 'vue' import UsageView from '../UsageView.vue' const { query, getStatsByDateRange, list, showError, showWarning, showSuccess, showInfo } = vi.hoisted(() => ({ query: vi.fn(), getStatsByDateRange: vi.fn(), list: vi.fn(), showError: vi.fn(), showWarning: vi.fn(), showSuccess: vi.fn(), showInfo: vi.fn(), })) const messages: Record = { 'usage.costDetails': 'Cost Breakdown', 'admin.usage.inputCost': 'Input Cost', 'admin.usage.outputCost': 'Output Cost', 'admin.usage.cacheCreationCost': 'Cache Creation Cost', 'admin.usage.cacheReadCost': 'Cache Read Cost', 'usage.inputTokenPrice': 'Input price', 'usage.outputTokenPrice': 'Output price', 'usage.perMillionTokens': '/ 1M tokens', 'usage.serviceTier': 'Service tier', 'usage.serviceTierPriority': 'Fast', 'usage.serviceTierFlex': 'Flex', 'usage.serviceTierStandard': 'Standard', 'usage.rate': 'Rate', 'usage.original': 'Original', 'usage.billed': 'Billed', 'usage.allApiKeys': 'All API Keys', 'usage.apiKeyFilter': 'API Key', 'usage.model': 'Model', 'usage.reasoningEffort': 'Reasoning Effort', 'usage.type': 'Type', 'usage.tokens': 'Tokens', 'usage.cost': 'Cost', 'usage.firstToken': 'First Token', 'usage.duration': 'Duration', 'usage.time': 'Time', 'usage.userAgent': 'User Agent', } vi.mock('@/api', () => ({ usageAPI: { query, getStatsByDateRange, }, keysAPI: { list, }, })) vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError, showWarning, showSuccess, showInfo }), })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') return { ...actual, useI18n: () => ({ t: (key: string) => messages[key] ?? key, }), } }) const AppLayoutStub = { template: '
' } const TablePageLayoutStub = { template: '
', } describe('user UsageView tooltip', () => { beforeEach(() => { query.mockReset() getStatsByDateRange.mockReset() list.mockReset() showError.mockReset() showWarning.mockReset() showSuccess.mockReset() showInfo.mockReset() vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ x: 0, y: 0, top: 20, left: 20, right: 120, bottom: 40, width: 100, height: 20, toJSON: () => ({}), } as DOMRect) ;(globalThis as any).ResizeObserver = class { observe() {} disconnect() {} } }) it('shows fast service tier and unit prices in user tooltip', async () => { query.mockResolvedValue({ items: [ { request_id: 'req-user-1', actual_cost: 0.092883, total_cost: 0.092883, rate_multiplier: 1, service_tier: 'priority', input_cost: 0.020285, output_cost: 0.00303, cache_creation_cost: 0, cache_read_cost: 0.069568, input_tokens: 4057, output_tokens: 101, cache_creation_tokens: 0, cache_read_tokens: 278272, cache_creation_5m_tokens: 0, cache_creation_1h_tokens: 0, image_count: 0, image_size: null, first_token_ms: null, duration_ms: 1, created_at: '2026-03-08T00:00:00Z', }, ], total: 1, pages: 1, }) getStatsByDateRange.mockResolvedValue({ total_requests: 1, total_tokens: 100, total_cost: 0.1, avg_duration_ms: 1, }) list.mockResolvedValue({ items: [] }) const wrapper = mount(UsageView, { global: { stubs: { AppLayout: AppLayoutStub, TablePageLayout: TablePageLayoutStub, Pagination: true, EmptyState: true, Select: true, DateRangePicker: true, Icon: true, Teleport: true, }, }, }) await flushPromises() await nextTick() const setupState = (wrapper.vm as any).$?.setupState setupState.tooltipData = { request_id: 'req-user-1', actual_cost: 0.092883, total_cost: 0.092883, rate_multiplier: 1, service_tier: 'priority', input_cost: 0.020285, output_cost: 0.00303, cache_creation_cost: 0, cache_read_cost: 0.069568, input_tokens: 4057, output_tokens: 101, } setupState.tooltipVisible = true await nextTick() const text = wrapper.text() expect(text).toContain('Service tier') expect(text).toContain('Fast') expect(text).toContain('Rate') expect(text).toContain('1.00x') expect(text).toContain('Billed') expect(text).toContain('$0.092883') expect(text).toContain('$5.0000 / 1M tokens') expect(text).toContain('$30.0000 / 1M tokens') }) it('exports csv with input and output unit price columns', async () => { const exportedLogs = [ { request_id: 'req-user-export', actual_cost: 0.092883, total_cost: 0.092883, rate_multiplier: 1, service_tier: 'priority', input_cost: 0.020285, output_cost: 0.00303, cache_creation_cost: 0.000001, cache_read_cost: 0.069568, input_tokens: 4057, output_tokens: 101, cache_creation_tokens: 4, cache_read_tokens: 278272, cache_creation_5m_tokens: 0, cache_creation_1h_tokens: 0, image_count: 0, image_size: null, first_token_ms: 12, duration_ms: 345, created_at: '2026-03-08T00:00:00Z', model: 'gpt-5.4', reasoning_effort: null, api_key: { name: 'demo-key' }, }, ] query.mockResolvedValue({ items: exportedLogs, total: 1, pages: 1, }) getStatsByDateRange.mockResolvedValue({ total_requests: 1, total_tokens: 100, total_cost: 0.1, avg_duration_ms: 1, }) list.mockResolvedValue({ items: [] }) let exportedBlob: Blob | null = null const originalCreateObjectURL = window.URL.createObjectURL const originalRevokeObjectURL = window.URL.revokeObjectURL window.URL.createObjectURL = vi.fn((blob: Blob | MediaSource) => { exportedBlob = blob as Blob return 'blob:usage-export' }) as typeof window.URL.createObjectURL window.URL.revokeObjectURL = vi.fn(() => {}) as typeof window.URL.revokeObjectURL const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) const wrapper = mount(UsageView, { global: { stubs: { AppLayout: AppLayoutStub, TablePageLayout: TablePageLayoutStub, Pagination: true, EmptyState: true, Select: true, DateRangePicker: true, Icon: true, Teleport: true, }, }, }) await flushPromises() const setupState = (wrapper.vm as any).$?.setupState await setupState.exportToCSV() expect(exportedBlob).not.toBeNull() expect(clickSpy).toHaveBeenCalled() expect(showSuccess).toHaveBeenCalled() window.URL.createObjectURL = originalCreateObjectURL window.URL.revokeObjectURL = originalRevokeObjectURL clickSpy.mockRestore() }) })