diff --git a/frontend/web/src/components/intelligent_eval/ConfigSnapshots.test.tsx b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.test.tsx
new file mode 100644
index 0000000..5597dfa
--- /dev/null
+++ b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.test.tsx
@@ -0,0 +1,116 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { ConfigSnapshot } from '../../api'
+import ConfigSnapshots from './ConfigSnapshots'
+
+const listConfigSnapshots = vi.fn()
+const getConfigSnapshot = vi.fn()
+const compareConfigSnapshots = vi.fn()
+const downloadJson = vi.fn()
+
+vi.mock('../../api', () => ({
+ intelligentEvalsApi: {
+ listConfigSnapshots: (...args: unknown[]) => listConfigSnapshots(...args),
+ getConfigSnapshot: (...args: unknown[]) => getConfigSnapshot(...args),
+ compareConfigSnapshots: (...args: unknown[]) => compareConfigSnapshots(...args),
+ },
+}))
+
+vi.mock('../../utils/download', () => ({
+ downloadJson: (...args: unknown[]) => downloadJson(...args),
+}))
+
+function makeSnapshot(id: string, type: ConfigSnapshot['snapshot_type'] = 'created'): ConfigSnapshot {
+ return {
+ id,
+ eval_id: 'eval-1',
+ snapshot_type: type,
+ goal: `评估目标 ${id}`,
+ seeds: {},
+ intent: '',
+ role_description: '',
+ time_window_hours: 24,
+ plan: null,
+ created_at: '2026-01-01T08:00:00Z',
+ created_by: 'openclaw',
+ }
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ cleanup()
+})
+
+describe('ConfigSnapshots', () => {
+ it('lists snapshots with type labels and shows empty state when none', async () => {
+ listConfigSnapshots.mockResolvedValue({ data: { snapshots: [makeSnapshot('snap-1'), makeSnapshot('snap-2', 'plan_submitted')] } })
+ render()
+
+ await waitFor(() => expect(listConfigSnapshots).toHaveBeenCalledWith('eval-1'))
+ expect(await screen.findByText('创建')).toBeInTheDocument()
+ expect(screen.getByText('计划提交')).toBeInTheDocument()
+ expect(screen.getAllByText('openclaw').length).toBe(2)
+
+ cleanup()
+ listConfigSnapshots.mockResolvedValue({ data: { snapshots: [] } })
+ render()
+ expect(await screen.findByText('暂无配置快照')).toBeInTheDocument()
+ })
+
+ it('opens snapshot detail and returns to the list', async () => {
+ const snapshot = makeSnapshot('snap-1')
+ listConfigSnapshots.mockResolvedValue({ data: { snapshots: [snapshot] } })
+ getConfigSnapshot.mockResolvedValue({ data: snapshot })
+ render()
+
+ fireEvent.click(await screen.findByText(/查\s*看/))
+ await waitFor(() => expect(getConfigSnapshot).toHaveBeenCalledWith('eval-1', 'snap-1'))
+ expect(await screen.findByText('快照详情')).toBeInTheDocument()
+ expect(screen.getByText('评估目标 snap-1')).toBeInTheDocument()
+ expect(screen.getByText('24 小时')).toBeInTheDocument()
+
+ fireEvent.click(screen.getByText('返回'))
+ expect(await screen.findByText('配置历史')).toBeInTheDocument()
+ })
+
+ it('exports a snapshot as JSON', async () => {
+ listConfigSnapshots.mockResolvedValue({ data: { snapshots: [makeSnapshot('snap-12345678')] } })
+ render()
+
+ fireEvent.click(await screen.findByText(/导\s*出/))
+ expect(downloadJson).toHaveBeenCalledWith(
+ expect.objectContaining({ id: 'snap-12345678' }),
+ 'config-snapshot-snap-123.json',
+ )
+ })
+
+ it('compares two selected snapshots and renders differences', async () => {
+ listConfigSnapshots.mockResolvedValue({
+ data: { snapshots: [makeSnapshot('snap-1'), makeSnapshot('snap-2', 'config_updated')] },
+ })
+ compareConfigSnapshots.mockResolvedValue({
+ data: {
+ snapshot_1: { id: 'snap-1', snapshot_type: 'created', created_at: '2026-01-01T08:00:00Z' },
+ snapshot_2: { id: 'snap-2', snapshot_type: 'config_updated', created_at: '2026-01-02T08:00:00Z' },
+ differences: { goal: { old: '旧目标', new: '新目标' } },
+ },
+ })
+ render()
+
+ await screen.findByText('创建')
+ // 第 0 个是表头全选框,行内勾选从索引 1 开始
+ const checkboxes = screen.getAllByRole('checkbox')
+ fireEvent.click(checkboxes[1])
+ fireEvent.click(checkboxes[2])
+ fireEvent.click(screen.getByText(/对比选中/))
+
+ await waitFor(() => expect(compareConfigSnapshots).toHaveBeenCalledWith('eval-1', 'snap-1', 'snap-2'))
+ expect(await screen.findByText('快照对比')).toBeInTheDocument()
+ expect(screen.getByText('goal')).toBeInTheDocument()
+ expect(screen.getByText('旧目标')).toBeInTheDocument()
+ expect(screen.getByText('新目标')).toBeInTheDocument()
+ })
+})
diff --git a/frontend/web/src/components/intelligent_eval/DecisionProcess.test.tsx b/frontend/web/src/components/intelligent_eval/DecisionProcess.test.tsx
new file mode 100644
index 0000000..50adc37
--- /dev/null
+++ b/frontend/web/src/components/intelligent_eval/DecisionProcess.test.tsx
@@ -0,0 +1,90 @@
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { DecisionLog } from '../../api'
+import type { ReadSlot } from '../../read/readResource'
+import DecisionProcess from './DecisionProcess'
+
+const downloadJson = vi.fn()
+
+vi.mock('../../utils/download', () => ({
+ downloadJson: (...args: unknown[]) => downloadJson(...args),
+}))
+
+function makeLog(i: number, type: DecisionLog['decision_type'] = 'execute_session'): DecisionLog {
+ return {
+ id: `log-${i}`,
+ eval_id: 'eval-1',
+ decision_type: type,
+ reason: `时段欠账 ${i}`,
+ context: { debt: i },
+ cron_id: `cron-${i}-abcdef`,
+ created_at: `2026-01-01T08:${String(i % 60).padStart(2, '0')}:00Z`,
+ }
+}
+
+function readySlot(logs: DecisionLog[]): ReadSlot {
+ return { phase: 'ready', value: logs, error: null }
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ cleanup()
+})
+
+describe('DecisionProcess', () => {
+ it('shows loading placeholder while the slot is loading', () => {
+ const slot: ReadSlot = { phase: 'loading', value: null, error: null }
+ render( {}} />)
+
+ expect(screen.getByText('加载中...')).toBeInTheDocument()
+ })
+
+ it('shows empty state when there are no logs', () => {
+ render( {}} />)
+
+ expect(screen.getAllByText('暂无决策日志').length).toBeGreaterThanOrEqual(1)
+ })
+
+ it('renders log reasons in the timeline and the table', () => {
+ render( {}} />)
+
+ expect(screen.getAllByText('时段欠账 1').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getAllByText('时段欠账 2').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getByText(/共 2 条/)).toBeInTheDocument()
+ })
+
+ it('expands a row to show decision context', async () => {
+ render( {}} />)
+
+ expect(screen.getAllByText('时段欠账 3').length).toBe(2)
+ expect(screen.queryByText('决策上下文')).not.toBeInTheDocument()
+ // antd 会在两个汉字的按钮文案中间插空格(详 情)
+ fireEvent.click(screen.getByText(/详\s*情/))
+ expect(await screen.findByText('决策上下文')).toBeInTheDocument()
+ expect(screen.getByText(/"debt": 3/)).toBeInTheDocument()
+ })
+
+ it('triggers onReload from the refresh button', () => {
+ const onReload = vi.fn()
+ render()
+
+ fireEvent.click(screen.getByText('刷新'))
+ expect(onReload).toHaveBeenCalledTimes(1)
+ })
+
+ it('exports logs as JSON and skips export when empty', () => {
+ const { unmount } = render(
+ {}} />,
+ )
+ fireEvent.click(screen.getByText(/导\s*出/))
+ expect(downloadJson).toHaveBeenCalledWith(expect.any(Array), 'decision-logs-eval-123.json')
+ unmount()
+
+ render( {}} />)
+ fireEvent.click(screen.getByText(/导\s*出/))
+ expect(downloadJson).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/frontend/web/src/components/intelligent_eval/EvalOverview.test.tsx b/frontend/web/src/components/intelligent_eval/EvalOverview.test.tsx
new file mode 100644
index 0000000..1b9a603
--- /dev/null
+++ b/frontend/web/src/components/intelligent_eval/EvalOverview.test.tsx
@@ -0,0 +1,129 @@
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it } from 'vitest'
+import type { IntelligentEval, IntelligentEvalSession } from '../../api'
+import EvalOverview from './EvalOverview'
+
+function makeEval(overrides: Partial = {}): IntelligentEval {
+ return {
+ id: 'eval-1',
+ name: '24h 服务质量评估',
+ target_id: 't-1',
+ status: 'pending_approval',
+ goal: '评估服务态度与专业度',
+ seeds: {},
+ intent: '重点关注价保场景',
+ role_description: '客服数字员工',
+ plan: null,
+ plan_feedback: null,
+ time_window_hours: 24,
+ report: null,
+ created_at: '2026-01-01T00:00:00Z',
+ updated_at: null,
+ started_at: null,
+ completed_at: null,
+ session_count: 4,
+ completed_sessions: 0,
+ sessions: [],
+ ...overrides,
+ }
+}
+
+function makeSession(overrides: Partial = {}): IntelligentEvalSession {
+ return {
+ id: 's-1',
+ eval_id: 'eval-1',
+ target_id: 't-1',
+ persona: { name: '老客户' },
+ goal: '完成退货',
+ dimension: '售后',
+ status: 'completed',
+ verdict: null,
+ turn_count: 5,
+ created_at: '2026-01-01T08:10:00Z',
+ closed_at: '2026-01-01T08:25:00Z',
+ ...overrides,
+ }
+}
+
+afterEach(() => {
+ cleanup()
+})
+
+describe('EvalOverview', () => {
+ it('renders basic info: target, status tag, time window and session progress', () => {
+ render()
+
+ expect(screen.getByText('tutu 客服通道')).toBeInTheDocument()
+ expect(screen.getByText('待审批')).toBeInTheDocument()
+ expect(screen.getByText('24 小时')).toBeInTheDocument()
+ expect(screen.getByText('0/4')).toBeInTheDocument()
+ expect(screen.getByText('评估服务态度与专业度')).toBeInTheDocument()
+ expect(screen.getByText('重点关注价保场景')).toBeInTheDocument()
+ })
+
+ it('shows planning hint while planning without feedback', () => {
+ render()
+
+ expect(screen.getByText(/OpenClaw 正在规划/)).toBeInTheDocument()
+ expect(screen.queryByText('粗计划')).not.toBeInTheDocument()
+ })
+
+ it('shows rejection alert when plan was sent back', () => {
+ render()
+
+ expect(screen.getByText(/计划已打回/)).toBeInTheDocument()
+ expect(screen.getByText('维度太粗,请细化')).toBeInTheDocument()
+ })
+
+ it('renders the plan summary for pending_approval', () => {
+ const ev = makeEval({
+ plan: {
+ dimensions: ['服务态度', '专业度'],
+ virtual_users: [{ persona: { name: '新客户' }, goal: '咨询价保' }],
+ time_distribution: [{ time_slot: '0-1h', sessions: 2, scenario: '早高峰咨询' }],
+ estimated_sessions: 4,
+ budget: { max_turns_per_session: 12, total_max_turns: 48 },
+ completion_criteria: '全部时段完成',
+ },
+ })
+ render()
+
+ expect(screen.getByText('粗计划')).toBeInTheDocument()
+ expect(screen.getByText('服务态度')).toBeInTheDocument()
+ expect(screen.getByText('专业度')).toBeInTheDocument()
+ expect(screen.getByText('新客户')).toBeInTheDocument()
+ // 目标与 persona 在同一个 div 里(新客户 · 咨询价保)
+ expect(screen.getByText((content) => content.includes('咨询价保'))).toBeInTheDocument()
+ expect(screen.getByText('0-1h')).toBeInTheDocument()
+ expect(screen.getByText('全部时段完成')).toBeInTheDocument()
+ })
+
+ it('renders session progress rows only while executing or completed', () => {
+ const sessions = [makeSession()]
+ const { unmount } = render(
+ ,
+ )
+ expect(screen.getByText(/会话进度(1\/1)/)).toBeInTheDocument()
+ expect(screen.getByText('老客户')).toBeInTheDocument()
+ expect(screen.getByText('完成退货')).toBeInTheDocument()
+ expect(screen.getByText('售后')).toBeInTheDocument()
+ unmount()
+
+ render()
+ expect(screen.queryByText(/会话进度(/)).not.toBeInTheDocument()
+ })
+
+ it('shows seeds JSON when provided and a dash when empty', () => {
+ const { unmount } = render(
+ ,
+ )
+ expect(screen.getByText(/怎么退货/)).toBeInTheDocument()
+ unmount()
+
+ render()
+ expect(screen.getByText('已显示全部内容')).toBeInTheDocument()
+ })
+})
diff --git a/frontend/web/src/components/intelligent_eval/EvalReport.test.tsx b/frontend/web/src/components/intelligent_eval/EvalReport.test.tsx
new file mode 100644
index 0000000..15da14a
--- /dev/null
+++ b/frontend/web/src/components/intelligent_eval/EvalReport.test.tsx
@@ -0,0 +1,139 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { IntelligentEval, IntelligentEvalReport } from '../../api'
+import EvalReport from './EvalReport'
+
+const downloadReportMarkdown = vi.fn()
+const listMessages = vi.fn()
+
+vi.mock('../../api', () => ({
+ intelligentEvalsApi: {
+ downloadReportMarkdown: (...args: unknown[]) => downloadReportMarkdown(...args),
+ listMessages: (...args: unknown[]) => listMessages(...args),
+ },
+}))
+
+function makeReport(overrides: Partial = {}): IntelligentEvalReport {
+ return {
+ summary: '整体服务态度良好,价保场景存在流程遗漏',
+ scores: { overall: 4.2, dimensions: { 服务态度: 4.5, 专业度: 3.2 } },
+ findings: [
+ {
+ issue: '价保流程未主动告知凭证',
+ severity: 'high',
+ dimension: '专业度',
+ evidence: [{ session_id: 'sess-aaaaaaaa', turn_index: 2, user_said: '需要什么凭证', assistant_replied: '您好' }],
+ suggestion: '补充 SOP 提示',
+ related_sop: 'SOP-12',
+ },
+ {
+ issue: '回复偏慢',
+ severity: 'medium',
+ dimension: '响应时效',
+ evidence: [],
+ },
+ ],
+ highlights: [{ description: '语气友好', dimension: '服务态度' }],
+ priority_recommendations: ['优先整改价保流程'],
+ ...overrides,
+ }
+}
+
+function makeEval(overrides: Partial = {}): IntelligentEval {
+ return {
+ id: 'eval-1',
+ name: '24h 评估',
+ target_id: 't-1',
+ status: 'completed',
+ goal: 'goal',
+ seeds: {},
+ intent: '',
+ role_description: '',
+ plan: null,
+ plan_feedback: null,
+ time_window_hours: 24,
+ report: makeReport(),
+ created_at: null,
+ updated_at: null,
+ started_at: null,
+ completed_at: '2026-01-02T00:00:00Z',
+ session_count: 1,
+ completed_sessions: 1,
+ sessions: [
+ {
+ id: 's-1',
+ eval_id: 'eval-1',
+ target_id: 't-1',
+ persona: { name: '老客户' },
+ goal: '完成退货',
+ dimension: '售后',
+ status: 'completed',
+ verdict: null,
+ turn_count: 5,
+ created_at: '2026-01-01T08:10:00Z',
+ closed_at: '2026-01-01T08:25:00Z',
+ },
+ ],
+ ...overrides,
+ }
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ cleanup()
+})
+
+describe('EvalReport', () => {
+ it('shows empty states when there is no report and no sessions', () => {
+ render()
+
+ expect(screen.getByText('暂无报告')).toBeInTheDocument()
+ expect(screen.getByText('暂无会话')).toBeInTheDocument()
+ })
+
+ it('renders summary cards, dimension scores, findings, highlights and recommendations', () => {
+ render()
+
+ expect(screen.getByText('4.2')).toBeInTheDocument()
+ expect(screen.getByText('问题发现(2)')).toBeInTheDocument()
+ expect(screen.getAllByText('服务态度').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getByText('4.5')).toBeInTheDocument()
+ expect(screen.getByText('3.2')).toBeInTheDocument()
+ expect(screen.getByText('整体服务态度良好,价保场景存在流程遗漏')).toBeInTheDocument()
+ expect(screen.getByText('价保流程未主动告知凭证')).toBeInTheDocument()
+ expect(screen.getByText('回复偏慢')).toBeInTheDocument()
+ expect(screen.getByText('语气友好')).toBeInTheDocument()
+ expect(screen.getByText('优先整改价保流程')).toBeInTheDocument()
+ expect(document.body.textContent).toContain('高 1 · 中 1 · 低 0')
+ })
+
+ it('expanding a session panel loads its messages', async () => {
+ listMessages.mockResolvedValue({
+ data: {
+ messages: [
+ { id: 'm-1', session_id: 's-1', role: 'user', content: '我想退货', latency_ms: null, created_at: null },
+ { id: 'm-2', session_id: 's-1', role: 'assistant', content: '好的,请提供订单号', latency_ms: 320, created_at: null },
+ ],
+ },
+ })
+ render()
+
+ expect(listMessages).not.toHaveBeenCalled()
+ fireEvent.click(screen.getByText('老客户'))
+ await waitFor(() => expect(listMessages).toHaveBeenCalledWith('eval-1', 's-1'))
+ expect(await screen.findByText('我想退货')).toBeInTheDocument()
+ expect(screen.getByText('好的,请提供订单号')).toBeInTheDocument()
+ expect(screen.getByText('320ms')).toBeInTheDocument()
+ })
+
+ it('exports the markdown report', async () => {
+ downloadReportMarkdown.mockResolvedValue(undefined)
+ render()
+
+ fireEvent.click(screen.getByText(/导出 Markdown/))
+ await waitFor(() => expect(downloadReportMarkdown).toHaveBeenCalledWith('eval-1'))
+ })
+})
diff --git a/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.test.tsx b/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.test.tsx
new file mode 100644
index 0000000..3bc7735
--- /dev/null
+++ b/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.test.tsx
@@ -0,0 +1,126 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { TaskQueueItem, TaskQueueList } from '../../api'
+import TaskQueueMonitor from './TaskQueueMonitor'
+
+const listTasks = vi.fn()
+
+vi.mock('../../api', () => ({
+ intelligentEvalsApi: {
+ listTasks: (...args: unknown[]) => listTasks(...args),
+ },
+}))
+
+function makeTask(id: string, status: TaskQueueItem['status'], overrides: Partial = {}): TaskQueueItem {
+ return {
+ id,
+ eval_id: `eval-${id}`,
+ eval_name: `评估 ${id}`,
+ eval_status: 'executing',
+ status,
+ priority: 1,
+ reason: `时段欠账 ${id}`,
+ assigned_cron_id: null,
+ assigned_at: null,
+ completed_at: status === 'completed' ? '2026-01-01T09:00:00Z' : null,
+ created_at: '2026-01-01T08:00:00Z',
+ updated_at: null,
+ error: null,
+ ...overrides,
+ }
+}
+
+function makeList(tasks: TaskQueueItem[], stats: TaskQueueList['stats']): TaskQueueList {
+ return { tasks, stats }
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+describe('TaskQueueMonitor', () => {
+ it('renders stat chips from stats without the derived unresolved chip', async () => {
+ listTasks.mockResolvedValue({
+ data: makeList([makeTask('1', 'pending')], { pending: 1, assigned: 2, completed: 3, failed: 4, unresolved: 3 }),
+ })
+ render()
+
+ await waitFor(() => expect(listTasks).toHaveBeenCalledTimes(1))
+ expect(await screen.findByText('全部任务')).toBeInTheDocument()
+ // 状态文案同时出现在 chip 与表格 Tag 中
+ expect(screen.getAllByText('待认领').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getAllByText('执行中').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getAllByText('已完成').length).toBeGreaterThanOrEqual(1)
+ expect(screen.getAllByText('失败').length).toBeGreaterThanOrEqual(1)
+ // 全部 = 四态之和,不含派生的 unresolved
+ expect(screen.getByText('10')).toBeInTheDocument()
+ expect(screen.queryByText('unresolved')).not.toBeInTheDocument()
+ })
+
+ it('renders task rows with eval name and queue status', async () => {
+ listTasks.mockResolvedValue({
+ data: makeList(
+ [
+ makeTask('1', 'completed'),
+ makeTask('2', 'failed', { error: 'worker 超时' }),
+ ],
+ { pending: 0, assigned: 0, completed: 1, failed: 1, unresolved: 0 },
+ ),
+ })
+ render()
+
+ expect(await screen.findByText('评估 1')).toBeInTheDocument()
+ expect(screen.getByText('评估 2')).toBeInTheDocument()
+ expect(screen.getByText('时段欠账 1')).toBeInTheDocument()
+ // 失败任务的完成时间列显示 失败(悬浮提示里是错误详情)
+ expect(screen.getAllByText('失败').length).toBeGreaterThanOrEqual(1)
+ })
+
+ it('filters rows by the clicked stat chip', async () => {
+ listTasks.mockResolvedValue({
+ data: makeList(
+ [makeTask('1', 'pending'), makeTask('2', 'completed')],
+ { pending: 1, assigned: 0, completed: 1, failed: 0, unresolved: 1 },
+ ),
+ })
+ render()
+
+ await screen.findByText('评估 1')
+ expect(screen.getByText('评估 2')).toBeInTheDocument()
+ // stat-bar 先于表格渲染,第一个 待认领 是筛选 chip
+ fireEvent.click(screen.getAllByText('待认领')[0])
+ expect(screen.getByText('评估 1')).toBeInTheDocument()
+ expect(screen.queryByText('评估 2')).not.toBeInTheDocument()
+ })
+
+ it('shows empty state when there are no tasks', async () => {
+ listTasks.mockResolvedValue({
+ data: makeList([], { pending: 0, assigned: 0, completed: 0, failed: 0, unresolved: 0 }),
+ })
+ render()
+
+ expect(await screen.findByText('暂无任务')).toBeInTheDocument()
+ })
+
+ it('polls every 5 seconds and reloads on demand', async () => {
+ vi.useFakeTimers()
+ listTasks.mockResolvedValue({
+ data: makeList([], { pending: 0, assigned: 0, completed: 0, failed: 0, unresolved: 0 }),
+ })
+ render()
+
+ await vi.advanceTimersByTimeAsync(0)
+ expect(listTasks).toHaveBeenCalledTimes(1)
+ await vi.advanceTimersByTimeAsync(5000)
+ expect(listTasks).toHaveBeenCalledTimes(2)
+ // 手动刷新按钮
+ fireEvent.click(screen.getByRole('button'))
+ await vi.advanceTimersByTimeAsync(0)
+ expect(listTasks).toHaveBeenCalledTimes(3)
+ })
+})