import { App as AntApp, ConfigProvider } from 'antd'; import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AppContext, type AppContextValue } from '../../src/frontend/app-context'; import { I18nProvider } from '../../src/frontend/i18n/react'; import { api } from '../../src/frontend/services/api'; import { mailhubTheme } from '../../src/frontend/theme'; import type { Analytics, ApiToken, RuntimeConfig, WebhookDelivery } from '../../src/frontend/types'; import Dashboard from '../../src/pages/Dashboard'; vi.mock('../../src/pages/DashboardCharts', () => ({ default: () =>
})); describe('Dashboard operational states', () => { beforeEach(() => { window.localStorage.removeItem('mailhub.locale'); }); afterEach(() => vi.restoreAllMocks()); it('keeps dependency failures visible without reporting a missing credential or all-clear state', async () => { const user = userEvent.setup(); mockCoreApis(); vi.spyOn(api, 'smtpCredential') .mockRejectedValueOnce(new Error('SMTP timeout')) .mockResolvedValue({ credential: null }); vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [sendToken] }); const deliveries = vi.spyOn(api, 'webhookDeliveries') .mockRejectedValueOnce(new Error('Webhook timeout')) .mockResolvedValue({ deliveries: [] }); renderDashboard(); expect(await screen.findByText('部分运维状态获取失败')).toBeTruthy(); expect(screen.getByText(/SMTP 凭据.*SMTP timeout/)).toBeTruthy(); expect(screen.getByText(/Webhook 失败投递.*Webhook timeout/)).toBeTruthy(); expect(screen.queryByText('尚未创建可用的发送凭据')).toBeNull(); expect(screen.queryByText('状态正常')).toBeNull(); expect(screen.getByText('状态未知')).toBeTruthy(); await user.click(screen.getByRole('button', { name: /重\s*试/ })); await waitFor(() => expect(screen.queryByText('部分运维状态获取失败')).toBeNull()); expect(await screen.findByText('状态正常')).toBeTruthy(); expect(deliveries).toHaveBeenCalledTimes(2); }); it('accepts SMTP or a send-scoped API key and exposes both setup entrances', async () => { const user = userEvent.setup(); mockCoreApis(); vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: null }); vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [] }); vi.spyOn(api, 'webhookDeliveries').mockResolvedValue({ deliveries: [] }); const router = renderDashboard(); expect(await screen.findByText('尚未创建可用的发送凭据')).toBeTruthy(); expect(screen.getByRole('button', { name: /配置 SMTP/ })).toBeTruthy(); expect(screen.getByRole('button', { name: /创建 API 密钥/ })).toBeTruthy(); expect(screen.getByRole('button', { name: 'SMTP' })).toBeTruthy(); expect(screen.getByRole('button', { name: 'API 密钥' })).toBeTruthy(); await user.click(screen.getByRole('button', { name: /创建 API 密钥/ })); await waitFor(() => expect(router.state.location.pathname).toBe('/integrations/api-keys')); }); it('does not flag credentials as missing when an active send-scoped API key exists', async () => { mockCoreApis(); vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: null }); vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [sendToken] }); vi.spyOn(api, 'webhookDeliveries').mockResolvedValue({ deliveries: [] }); renderDashboard(); expect(await screen.findByText('状态正常')).toBeTruthy(); expect(screen.queryByText('尚未创建可用的发送凭据')).toBeNull(); }); it('explains the deployment-level ADMIN_PASSWORD change without linking to system settings', async () => { mockHealthyDependencies(); renderDashboard({ usingDefaultAdminPassword: true }); const warning = (await screen.findByText('当前仍在使用默认管理员密码')).closest('.ant-alert'); expect(warning).not.toBeNull(); expect(within(warning as HTMLElement).getByText(/ADMIN_PASSWORD/)).toBeTruthy(); expect(within(warning as HTMLElement).getByText(/重启 MailHub/)).toBeTruthy(); expect(within(warning as HTMLElement).queryByRole('link')).toBeNull(); }); it('links a failed webhook warning to that webhook dead-delivery filter', async () => { const user = userEvent.setup(); mockCoreApis(); vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: { username: 'smtp-user', passwordSet: true } }); vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [] }); const deliveries = vi.spyOn(api, 'webhookDeliveries').mockResolvedValue({ deliveries: [deadDelivery] }); const router = renderDashboard(); await user.click(await screen.findByRole('button', { name: /检查 Webhooks/ })); await waitFor(() => expect(router.state.location.pathname).toBe('/integrations/webhooks')); const params = new URLSearchParams(router.state.location.search); expect(params.get('webhookId')).toBe('42'); expect(params.get('deliveryStatus')).toBe('dead'); expect(deliveries).toHaveBeenCalledWith({ status: 'dead', limit: 5 }); }); it('never labels analytics from the previous range as the newly selected range after a failed refresh', async () => { const user = userEvent.setup(); const previousRangeAnalytics: Analytics = { ...analytics, summary: { ...analytics.summary, total: 712345 } }; const analyticsRequest = vi.spyOn(api, 'analytics') .mockResolvedValueOnce({ analytics: previousRangeAnalytics }) .mockRejectedValueOnce(new Error('Analytics service unavailable')); vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'smtpCredential').mockResolvedValue({ credential: { username: 'smtp-user', passwordSet: true } }); vi.spyOn(api, 'apiTokens').mockResolvedValue({ tokens: [] }); vi.spyOn(api, 'webhookDeliveries').mockResolvedValue({ deliveries: [] }); renderDashboard(); expect(await screen.findByText('712345')).toBeTruthy(); await user.click(screen.getByText('30 天')); expect(await screen.findByText('概览加载失败')).toBeTruthy(); expect(screen.getByText(/投递统计.*Analytics service unavailable/)).toBeTruthy(); expect(screen.queryByText('712345')).toBeNull(); expect(analyticsRequest).toHaveBeenLastCalledWith(30); }); }); function renderDashboard(configPatch: Partial