test: add tests for prommetrics, common routes, and Sora admin page
- Add prommetrics package tests (12 tests covering all metric functions) - Add routes/common_test.go with health check, readiness, liveness tests - Add SoraAdminView.spec.ts with 11 component tests
This commit is contained in:
104
backend/internal/prommetrics/metrics_test.go
Normal file
104
backend/internal/prommetrics/metrics_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package prommetrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRegistryNotNil(t *testing.T) {
|
||||
if Registry == nil {
|
||||
t.Fatal("Registry should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDBConnections(t *testing.T) {
|
||||
// Should not panic
|
||||
SetDBConnections(10, 5)
|
||||
SetDBConnections(0, 0)
|
||||
}
|
||||
|
||||
func TestSetRedisConnections(t *testing.T) {
|
||||
// Should not panic
|
||||
SetRedisConnections(20, 8)
|
||||
SetRedisConnections(0, 0)
|
||||
}
|
||||
|
||||
func TestSetActiveAccounts(t *testing.T) {
|
||||
SetActiveAccounts(100)
|
||||
SetActiveAccounts(0)
|
||||
}
|
||||
|
||||
func TestSetRequestQueueDepth(t *testing.T) {
|
||||
SetRequestQueueDepth(50)
|
||||
SetRequestQueueDepth(0)
|
||||
}
|
||||
|
||||
func TestSetOpsHealthScore(t *testing.T) {
|
||||
SetOpsHealthScore(95)
|
||||
SetOpsHealthScore(0)
|
||||
SetOpsHealthScore(100)
|
||||
}
|
||||
|
||||
func TestSetErrorRate(t *testing.T) {
|
||||
SetErrorRate(0.05)
|
||||
SetErrorRate(0)
|
||||
SetErrorRate(1.0)
|
||||
}
|
||||
|
||||
func TestSetSuccessRate(t *testing.T) {
|
||||
SetSuccessRate(0.95)
|
||||
SetSuccessRate(0)
|
||||
SetSuccessRate(1.0)
|
||||
}
|
||||
|
||||
func TestSetQPS(t *testing.T) {
|
||||
SetQPS(123.45)
|
||||
SetQPS(0)
|
||||
}
|
||||
|
||||
func TestSetTPS(t *testing.T) {
|
||||
SetTPS(4567.89)
|
||||
SetTPS(0)
|
||||
}
|
||||
|
||||
func TestRecordHTTPRequest(t *testing.T) {
|
||||
RecordHTTPRequest("GET", "/api/v1/chat", 200, 100*time.Millisecond)
|
||||
RecordHTTPRequest("POST", "/api/v1/sora/generate", 201, 200*time.Millisecond)
|
||||
RecordHTTPRequest("GET", "/api/v1/models", 500, 50*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestMetricsRegistered(t *testing.T) {
|
||||
// Verify all metrics are registered by gathering them
|
||||
mfs, err := Registry.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to gather metrics: %v", err)
|
||||
}
|
||||
|
||||
expectedMetrics := map[string]bool{
|
||||
"sub2api_http_requests_total": false,
|
||||
"sub2api_http_request_duration_seconds": false,
|
||||
"sub2api_db_connections_active": false,
|
||||
"sub2api_db_connections_idle": false,
|
||||
"sub2api_redis_connections_total": false,
|
||||
"sub2api_redis_connections_idle": false,
|
||||
"sub2api_accounts_active_total": false,
|
||||
"sub2api_request_queue_depth": false,
|
||||
"sub2api_ops_health_score": false,
|
||||
"sub2api_error_rate": false,
|
||||
"sub2api_success_rate": false,
|
||||
"sub2api_qps": false,
|
||||
"sub2api_tps": false,
|
||||
}
|
||||
|
||||
for _, mf := range mfs {
|
||||
if _, ok := expectedMetrics[mf.GetName()]; ok {
|
||||
expectedMetrics[mf.GetName()] = true
|
||||
}
|
||||
}
|
||||
|
||||
for name, found := range expectedMetrics {
|
||||
if !found {
|
||||
t.Errorf("Expected metric %q not found in registry", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
241
backend/internal/server/routes/common_test.go
Normal file
241
backend/internal/server/routes/common_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegisterCommonRoutes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
RegisterCommonRoutes(r)
|
||||
|
||||
// Test /health
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Test /live
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/live", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Test /ready (no health checker set)
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/ready", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Test /metrics
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Test /setup/status
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/setup/status", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var setupResp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &setupResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(0), setupResp["code"])
|
||||
|
||||
// Test POST /api/event_logging/batch
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/event_logging/batch", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestHealthHandler_NoHealthChecker(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
healthHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", resp["status"])
|
||||
|
||||
components, ok := resp["components"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
db, ok := components["database"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "unknown", db["status"])
|
||||
}
|
||||
|
||||
func TestReadinessHandler_NoHealthChecker(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/ready", nil)
|
||||
|
||||
readinessHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ready", resp["status"])
|
||||
}
|
||||
|
||||
func TestLivenessHandler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/live", nil)
|
||||
|
||||
livenessHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "alive", resp["status"])
|
||||
|
||||
// Verify timestamp is valid
|
||||
ts, ok := resp["timestamp"].(string)
|
||||
assert.True(t, ok)
|
||||
_, err = time.Parse(time.RFC3339, ts)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// mockHealthChecker implements HealthChecker for testing
|
||||
type mockHealthChecker struct {
|
||||
dbHealthy bool
|
||||
redisHealthy bool
|
||||
}
|
||||
|
||||
func (m *mockHealthChecker) CheckDatabase() bool { return m.dbHealthy }
|
||||
func (m *mockHealthChecker) CheckRedis() bool { return m.redisHealthy }
|
||||
|
||||
func TestHealthHandler_WithHealthyChecker(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// Reset the healthChecker for test
|
||||
healthChecker = &mockHealthChecker{dbHealthy: true, redisHealthy: true}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
healthHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", resp["status"])
|
||||
}
|
||||
|
||||
func TestHealthHandler_WithUnhealthyDB(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
healthChecker = &mockHealthChecker{dbHealthy: false, redisHealthy: true}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
healthHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "degraded", resp["status"])
|
||||
}
|
||||
|
||||
func TestHealthHandler_WithUnhealthyRedis(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
healthChecker = &mockHealthChecker{dbHealthy: true, redisHealthy: false}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
healthHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "degraded", resp["status"])
|
||||
}
|
||||
|
||||
func TestReadinessHandler_AllHealthy(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
healthChecker = &mockHealthChecker{dbHealthy: true, redisHealthy: true}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/ready", nil)
|
||||
|
||||
readinessHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ready", resp["status"])
|
||||
}
|
||||
|
||||
func TestReadinessHandler_NotReady(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
healthChecker = &mockHealthChecker{dbHealthy: true, redisHealthy: false}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/ready", nil)
|
||||
|
||||
readinessHandler(c)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "not_ready", resp["status"])
|
||||
}
|
||||
|
||||
func TestDelegatedMetricFunctions(t *testing.T) {
|
||||
// These should not panic - they delegate to prommetrics package
|
||||
RecordHTTPRequest("GET", "/test", 200, 100*time.Millisecond)
|
||||
SetDBConnections(5, 3)
|
||||
SetRedisConnections(10, 4)
|
||||
SetActiveAccounts(50)
|
||||
SetRequestQueueDepth(5)
|
||||
SetOpsHealthScore(90)
|
||||
SetErrorRate(0.1)
|
||||
SetSuccessRate(0.9)
|
||||
SetQPS(100.0)
|
||||
SetTPS(500.0)
|
||||
}
|
||||
260
frontend/src/views/admin/__tests__/SoraAdminView.spec.ts
Normal file
260
frontend/src/views/admin/__tests__/SoraAdminView.spec.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import SoraAdminView from '../SoraAdminView.vue'
|
||||
import type { SoraSystemStats, SoraUserStats, SoraGenerationAdmin } from '@/api/admin/sora'
|
||||
|
||||
// 使用 vi.hoisted 确保 mock 函数在 mock 工厂函数执行前定义
|
||||
const {
|
||||
mockGetSystemStats,
|
||||
mockListUserStats,
|
||||
mockListGenerations,
|
||||
mockClearUserStorage
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetSystemStats: vi.fn(),
|
||||
mockListUserStats: vi.fn(),
|
||||
mockListGenerations: vi.fn(),
|
||||
mockClearUserStorage: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin/sora', () => ({
|
||||
default: {
|
||||
getSystemStats: mockGetSystemStats,
|
||||
listUserStats: mockListUserStats,
|
||||
listGenerations: mockListGenerations,
|
||||
clearUserStorage: mockClearUserStorage
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Stub child components
|
||||
const AppLayoutStub = defineComponent({
|
||||
name: 'AppLayout',
|
||||
template: '<div class="app-layout"><slot /></div>'
|
||||
})
|
||||
|
||||
const LoadingSpinnerStub = defineComponent({
|
||||
name: 'LoadingSpinner',
|
||||
template: '<div class="loading-spinner" />'
|
||||
})
|
||||
|
||||
const IconStub = defineComponent({
|
||||
name: 'Icon',
|
||||
props: ['name', 'size'],
|
||||
template: '<span class="icon" />'
|
||||
})
|
||||
|
||||
const BaseButtonStub = defineComponent({
|
||||
name: 'BaseButton',
|
||||
props: ['variant', 'size'],
|
||||
emits: ['click'],
|
||||
template: '<button @click="$emit(\'click\')"><slot /></button>'
|
||||
})
|
||||
|
||||
function createMockSystemStats(overrides: Partial<SoraSystemStats> = {}): SoraSystemStats {
|
||||
return {
|
||||
total_users: 10,
|
||||
total_generations: 100,
|
||||
total_storage_bytes: 5 * 1024 * 1024 * 1024,
|
||||
active_generations: 3,
|
||||
by_status: { completed: 80, failed: 15, pending: 5 },
|
||||
by_model: { sora2: 60, sora1: 40 },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockUserStats(): SoraUserStats[] {
|
||||
return [
|
||||
{
|
||||
user_id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
quota_bytes: 10 * 1024 * 1024 * 1024,
|
||||
used_bytes: 2 * 1024 * 1024 * 1024,
|
||||
available_bytes: 8 * 1024 * 1024 * 1024,
|
||||
quota_source: 'user',
|
||||
generations_count: 20,
|
||||
active_count: 1,
|
||||
total_file_size_bytes: 1 * 1024 * 1024 * 1024
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function createMockGenerations(): SoraGenerationAdmin[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
model: 'sora2',
|
||||
prompt: 'A beautiful sunset',
|
||||
media_type: 'video',
|
||||
status: 'completed',
|
||||
storage_type: 's3',
|
||||
media_url: 'https://example.com/video.mp4',
|
||||
file_size_bytes: 10 * 1024 * 1024,
|
||||
error_message: '',
|
||||
created_at: '2024-01-01T10:00:00Z',
|
||||
completed_at: '2024-01-01T10:05:00Z'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
describe('SoraAdminView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSystemStats.mockResolvedValue(createMockSystemStats())
|
||||
mockListUserStats.mockResolvedValue({
|
||||
items: createMockUserStats(),
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1
|
||||
})
|
||||
mockListGenerations.mockResolvedValue({
|
||||
items: createMockGenerations(),
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1
|
||||
})
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
return mount(SoraAdminView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: AppLayoutStub,
|
||||
LoadingSpinner: LoadingSpinnerStub,
|
||||
Icon: IconStub
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('初始加载', () => {
|
||||
it('加载时显示 loading spinner', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.find('.loading-spinner').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('加载完成后显示页面内容', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.loading-spinner').exists()).toBe(false)
|
||||
expect(wrapper.find('.app-layout').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('加载完成后调用所有 API', async () => {
|
||||
mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(mockGetSystemStats).toHaveBeenCalled()
|
||||
expect(mockListUserStats).toHaveBeenCalled()
|
||||
expect(mockListGenerations).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('概览标签页', () => {
|
||||
it('默认显示概览标签页', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
// 概览标签页应显示统计卡片
|
||||
const cards = wrapper.findAll('.card')
|
||||
expect(cards.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('显示系统统计数据', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('10') // total_users
|
||||
expect(wrapper.text()).toContain('100') // total_generations
|
||||
expect(wrapper.text()).toContain('3') // active_generations
|
||||
})
|
||||
|
||||
it('显示按状态分布', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('completed')
|
||||
expect(wrapper.text()).toContain('80')
|
||||
})
|
||||
|
||||
it('显示按模型分布', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('sora2')
|
||||
expect(wrapper.text()).toContain('60')
|
||||
})
|
||||
})
|
||||
|
||||
describe('用户统计标签页', () => {
|
||||
it('点击用户统计标签切换到用户列表', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
const tabs = wrapper.findAll('button')
|
||||
const userTab = tabs.find(b => b.text().includes('admin.sora.userStats'))
|
||||
await userTab?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('table').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('user1')
|
||||
expect(wrapper.text()).toContain('user1@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('生成记录标签页', () => {
|
||||
it('点击生成记录标签切换到记录列表', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
const tabs = wrapper.findAll('button')
|
||||
const genTab = tabs.find(b => b.text().includes('admin.sora.generations'))
|
||||
await genTab?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('table').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('user1')
|
||||
expect(wrapper.text()).toContain('sora2')
|
||||
expect(wrapper.text()).toContain('completed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API 错误处理', () => {
|
||||
it('API 错误时不会崩溃', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockGetSystemStats.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('格式化函数', () => {
|
||||
it('正确格式化字节数', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
// 5 GB 应该显示为 "5 GB"
|
||||
expect(wrapper.text()).toContain('5 GB')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user