// @vitest-environment jsdom /** * Sidebar 组件测试(v0.8.2 P3-3 补齐设置面板/侧栏测试空白) * * 覆盖契约: * - 挂载时拉取会话列表并渲染标题 * - v0.8.2 P2-4: session:updated 实时刷新 messageCount/updatedAt/title * - 搜索框(标题 + 内容全文搜索入口)存在 */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Sidebar } from '../Sidebar'; import { useSessionStore } from '@renderer/stores/session-store'; const noop = (): void => {}; const sessionUpdatedHandlers: Array<(data: unknown) => void> = []; function makeSession(overrides: Record = {}): { id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; totalTokens: number; pinned: boolean; archived: boolean; } { return { id: 's1', title: '第一个会话', createdAt: Date.now(), updatedAt: Date.now(), messageCount: 2, totalTokens: 0, pinned: false, archived: false, ...overrides, }; } beforeEach(() => { sessionUpdatedHandlers.length = 0; useSessionStore.getState().setSessions([]); useSessionStore.getState().setCurrentSession(null); const bridge = (window as unknown as { metona: Record }).metona; bridge.sessions = { ...(bridge.sessions as Record), list: vi.fn().mockResolvedValue([makeSession(), makeSession({ id: 's2', title: 'Second' })]), searchContent: vi.fn().mockResolvedValue({ success: true, data: [] }), onSessionUpdated: vi.fn((cb: (data: unknown) => void) => { sessionUpdatedHandlers.push(cb); return noop; }), }; }); describe('Sidebar — 会话列表', () => { it('挂载时拉取并渲染会话标题', async () => { render(); await waitFor(() => { expect(screen.getByText('第一个会话')).toBeInTheDocument(); expect(screen.getByText('Second')).toBeInTheDocument(); }); }); it('渲染搜索框与新建会话按钮', () => { render(); expect(screen.getByPlaceholderText(/搜索|Search/)).toBeInTheDocument(); expect(screen.getAllByRole('button').length).toBeGreaterThan(0); }); it('接收 session:updated 后实时刷新条数(P2-4)', async () => { render(); await waitFor(() => { expect(screen.getByText('第一个会话')).toBeInTheDocument(); }); expect(sessionUpdatedHandlers.length).toBeGreaterThan(0); act(() => { sessionUpdatedHandlers.forEach((h) => h({ sessionId: 's1', messageCount: 42, updatedAt: Date.now() }), ); }); await waitFor(() => { // t('sidebar.messageCount') zh 文案为 "{{count}} 条" expect(screen.getByText(/42\s*条|42 messages/)).toBeInTheDocument(); }); }); it('接收 session:updated 的 title 补丁 → 标题刷新(LLM 标题生成链路)', async () => { render(); await waitFor(() => { expect(screen.getByText('第一个会话')).toBeInTheDocument(); }); act(() => { sessionUpdatedHandlers.forEach((h) => h({ sessionId: 's1', title: 'LLM 生成的新标题' })); }); await waitFor(() => { expect(screen.getByText('LLM 生成的新标题')).toBeInTheDocument(); }); }); }); describe('Sidebar — 搜索', () => { it('输入搜索词触发内容全文搜索(FTS5 通道,300ms 防抖)', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText(/搜索|Search/); await user.type(input, '关键词'); await waitFor( () => { const bridge = (window as unknown as { metona: Record }).metona; expect(bridge.sessions).toBeDefined(); expect( (bridge.sessions as { searchContent: { mock: { calls: unknown[][] } } }).searchContent .mock.calls.length, ).toBeGreaterThan(0); }, { timeout: 2_000 }, ); }); });