126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
// @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<string, unknown> = {}): {
|
||
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<string, unknown> }).metona;
|
||
bridge.sessions = {
|
||
...(bridge.sessions as Record<string, unknown>),
|
||
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(<Sidebar />);
|
||
await waitFor(() => {
|
||
expect(screen.getByText('第一个会话')).toBeInTheDocument();
|
||
expect(screen.getByText('Second')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
it('渲染搜索框与新建会话按钮', () => {
|
||
render(<Sidebar />);
|
||
expect(screen.getByPlaceholderText(/搜索|Search/)).toBeInTheDocument();
|
||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('接收 session:updated 后实时刷新条数(P2-4)', async () => {
|
||
render(<Sidebar />);
|
||
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(<Sidebar />);
|
||
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(<Sidebar />);
|
||
const input = screen.getByPlaceholderText(/搜索|Search/);
|
||
await user.type(input, '关键词');
|
||
await waitFor(
|
||
() => {
|
||
const bridge = (window as unknown as { metona: Record<string, unknown> }).metona;
|
||
expect(bridge.sessions).toBeDefined();
|
||
expect(
|
||
(bridge.sessions as { searchContent: { mock: { calls: unknown[][] } } }).searchContent
|
||
.mock.calls.length,
|
||
).toBeGreaterThan(0);
|
||
},
|
||
{ timeout: 2_000 },
|
||
);
|
||
});
|
||
});
|