返回 oh-my-ppt
token-usage-page.test.ts
根目录 / tests / unit / settings / token-usage-page.test.ts
1 // @vitest-environment happy-dom
2
3 import React, { act } from 'react'
4 import { createRoot } from 'react-dom/client'
5 import { MemoryRouter } from 'react-router-dom'
6 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
8 const tokenUsagePageState = vi.hoisted(() => ({
9 destroy: vi.fn(),
10 translate: (key: string) =>
11 key === 'settings.usagePageTitle'
12 ? 'Token 用量统计'
13 : key === 'settings.usageTotalTokens'
14 ? '总 Token'
15 : key === 'settings.usageInputTokens'
16 ? '输入 Token'
17 : key === 'settings.usageOutputTokens'
18 ? '输出 Token'
19 : key === 'settings.usageCalls'
20 ? '模型调用'
21 : key,
22 navTranslate: (key: string) => (key === 'nav.tokenUsage' ? 'Token 用量' : key),
23 getAppVersion: vi.fn(async () => ({ version: '2.0.16' })),
24 getModelUsage: vi.fn(async () => ({
25 period: '30d' as const,
26 startedAt: 1,
27 totals: {
28 callCount: 3,
29 exactCallCount: 2,
30 estimatedCallCount: 1,
31 inputTokens: 1200,
32 outputTokens: 300,
33 totalTokens: 1500
34 },
35 byModel: [
36 {
37 provider: 'openai',
38 model: 'test-model',
39 callCount: 3,
40 exactCallCount: 2,
41 estimatedCallCount: 1,
42 inputTokens: 1200,
43 outputTokens: 300,
44 totalTokens: 1500
45 }
46 ],
47 byDay: [
48 {
49 date: '2026-06-15',
50 callCount: 3,
51 exactCallCount: 2,
52 estimatedCallCount: 1,
53 inputTokens: 1200,
54 outputTokens: 300,
55 totalTokens: 1500
56 }
57 ],
58 byHour: Array.from({ length: 24 }, (_, hour) => ({
59 hour,
60 callCount: hour === 10 ? 1 : 0,
61 exactCallCount: hour === 10 ? 1 : 0,
62 estimatedCallCount: 0,
63 inputTokens: hour === 10 ? 400 : 0,
64 outputTokens: hour === 10 ? 100 : 0,
65 totalTokens: hour === 10 ? 500 : 0
66 }))
67 }))
68 }))
69
70 vi.mock('chart.js/auto', () => ({
71 default: class ChartMock {
72 destroy = tokenUsagePageState.destroy
73 }
74 }))
75
76 vi.mock('../../../src/renderer/src/lib/ipc', () => ({
77 ipc: {
78 getAppVersion: tokenUsagePageState.getAppVersion,
79 getModelUsage: tokenUsagePageState.getModelUsage
80 }
81 }))
82
83 vi.mock('../../../src/renderer/src/i18n', () => ({
84 useLang: () => ({
85 t: tokenUsagePageState.translate
86 }),
87 useT: () => tokenUsagePageState.navTranslate
88 }))
89
90 import { Sidebar } from '../../../src/renderer/src/components/layout/Sidebar'
91 import { TokenUsagePage } from '../../../src/renderer/src/pages/token-usage'
92
93 describe('token usage route', () => {
94 let container: HTMLDivElement
95
96 beforeEach(() => {
97 globalThis.IS_REACT_ACT_ENVIRONMENT = true
98 container = document.createElement('div')
99 document.body.appendChild(container)
100 tokenUsagePageState.getModelUsage.mockClear()
101 })
102
103 afterEach(() => {
104 container.remove()
105 })
106
107 it('renders usage totals and charts from the usage IPC', async () => {
108 const root = createRoot(container)
109 await act(async () => {
110 root.render(
111 React.createElement(
112 MemoryRouter,
113 { initialEntries: ['/token-usage'] },
114 React.createElement(TokenUsagePage)
115 )
116 )
117 })
118
119 expect(container.textContent).toContain('Token 用量统计')
120 expect(container.textContent).toContain('1,500')
121 expect(container.querySelectorAll('canvas')).toHaveLength(3)
122 expect(tokenUsagePageState.getModelUsage).toHaveBeenCalledWith('30d')
123
124 await act(async () => root.unmount())
125 })
126
127 it('exposes the dedicated route from the sidebar', async () => {
128 const root = createRoot(container)
129 await act(async () => {
130 root.render(
131 React.createElement(
132 MemoryRouter,
133 { initialEntries: ['/token-usage'] },
134 React.createElement(Sidebar)
135 )
136 )
137 })
138
139 const usageLink = container.querySelector('a[href="/token-usage"]')
140 expect(usageLink).not.toBeNull()
141 expect(usageLink?.textContent).toContain('Token 用量')
142
143 await act(async () => root.unmount())
144 })
145 })
146
146 lines TYPESCRIPT