- 登录页和侧栏统一使用根目录 logo_square.png,并更新登录系统名称与副标题。 - 更新 Dashboard、项目库和工作区时间轴文案,移除底层时序视频图层说明。 - 演示视频项目显示名改为“演视LC视频序列”,启动时兼容迁移旧 Data_MyVideo_1 名称,恢复出厂设置使用新名。 - 调整侧栏用户管理入口为用户图标,底部当前用户入口为退出图标,并让退出提示不接收鼠标事件。 - 补充前端组件测试、后端演示重置测试和文档说明。
278 lines
8.3 KiB
TypeScript
278 lines
8.3 KiB
TypeScript
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import { Dashboard } from './Dashboard';
|
||
|
||
const apiMock = vi.hoisted(() => ({
|
||
getDashboardOverview: vi.fn(),
|
||
cancelTask: vi.fn(),
|
||
retryTask: vi.fn(),
|
||
getTask: vi.fn(),
|
||
}));
|
||
|
||
const wsMock = vi.hoisted(() => {
|
||
const state = {
|
||
callback: undefined as undefined | ((data: any) => void),
|
||
statusCallback: undefined as undefined | ((status: any) => void),
|
||
connected: false,
|
||
};
|
||
return {
|
||
state,
|
||
progressWS: {
|
||
connect: vi.fn(() => { state.connected = true; }),
|
||
disconnect: vi.fn(() => { state.connected = false; }),
|
||
isConnected: vi.fn(() => state.connected),
|
||
onProgress: vi.fn((cb: (data: any) => void) => {
|
||
state.callback = cb;
|
||
return vi.fn();
|
||
}),
|
||
onStatus: vi.fn((cb: (status: any) => void) => {
|
||
state.statusCallback = cb;
|
||
cb(state.connected ? 'connected' : 'disconnected');
|
||
return vi.fn();
|
||
}),
|
||
},
|
||
};
|
||
});
|
||
|
||
vi.mock('../lib/websocket', () => ({
|
||
progressWS: wsMock.progressWS,
|
||
}));
|
||
|
||
vi.mock('../lib/api', () => ({
|
||
getDashboardOverview: apiMock.getDashboardOverview,
|
||
cancelTask: apiMock.cancelTask,
|
||
retryTask: apiMock.retryTask,
|
||
getTask: apiMock.getTask,
|
||
}));
|
||
|
||
describe('Dashboard', () => {
|
||
beforeEach(() => {
|
||
vi.useRealTimers();
|
||
vi.clearAllMocks();
|
||
wsMock.state.connected = false;
|
||
wsMock.state.callback = undefined;
|
||
wsMock.state.statusCallback = undefined;
|
||
apiMock.getDashboardOverview.mockResolvedValue({
|
||
summary: {
|
||
project_count: 2,
|
||
parsing_task_count: 1,
|
||
annotation_count: 5,
|
||
frame_count: 100,
|
||
template_count: 3,
|
||
system_load_percent: 12,
|
||
},
|
||
tasks: [
|
||
{
|
||
id: 'project-1',
|
||
project_id: 1,
|
||
name: '真实项目.mp4',
|
||
progress: 60,
|
||
status: 'pending',
|
||
raw_status: 'running',
|
||
error: null,
|
||
frame_count: 10,
|
||
updated_at: '2026-05-01T00:00:00Z',
|
||
},
|
||
],
|
||
activity: [
|
||
{
|
||
id: 'activity-1',
|
||
kind: 'project',
|
||
time: '2026-05-01T00:00:00Z',
|
||
message: '项目状态: pending',
|
||
project: '真实项目.mp4',
|
||
},
|
||
],
|
||
});
|
||
});
|
||
|
||
it('loads dashboard stats, tasks, and activity from the backend overview endpoint', async () => {
|
||
render(<Dashboard />);
|
||
|
||
await waitFor(() => expect(apiMock.getDashboardOverview).toHaveBeenCalled());
|
||
expect(screen.getByText('项目总数')).toBeInTheDocument();
|
||
expect(screen.getByText('已存标注')).toBeInTheDocument();
|
||
expect(screen.getByText('真实项目.mp4')).toBeInTheDocument();
|
||
expect(screen.getByText('项目状态: pending')).toBeInTheDocument();
|
||
expect(screen.getByText('系统全局数据监控')).toBeInTheDocument();
|
||
expect(screen.queryByText('City_Driving_Dataset_004.mp4')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('keeps a recently completed task visible in the progress panel', async () => {
|
||
apiMock.getDashboardOverview.mockResolvedValueOnce({
|
||
summary: {
|
||
project_count: 1,
|
||
parsing_task_count: 0,
|
||
annotation_count: 0,
|
||
frame_count: 120,
|
||
template_count: 1,
|
||
system_load_percent: 8,
|
||
},
|
||
tasks: [
|
||
{
|
||
id: 'task-20',
|
||
task_id: 20,
|
||
project_id: 1,
|
||
name: 'completed.mp4',
|
||
progress: 100,
|
||
status: '解析完成',
|
||
raw_status: 'success',
|
||
error: null,
|
||
frame_count: 120,
|
||
updated_at: '2026-05-01T00:00:00Z',
|
||
},
|
||
],
|
||
activity: [],
|
||
});
|
||
|
||
render(<Dashboard />);
|
||
|
||
expect(await screen.findByText('任务进度 (当前 / 最近)')).toBeInTheDocument();
|
||
expect(screen.getByText('completed.mp4')).toBeInTheDocument();
|
||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||
expect(screen.getByText('解析完成')).toBeInTheDocument();
|
||
expect(screen.queryByText(/当前无处理任务/)).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('connects to the progress stream and updates progress tasks', async () => {
|
||
render(<Dashboard />);
|
||
|
||
await waitFor(() => expect(wsMock.progressWS.connect).toHaveBeenCalled());
|
||
|
||
act(() => {
|
||
wsMock.state.callback?.({
|
||
type: 'progress',
|
||
taskId: 'task-1',
|
||
projectName: 'demo.mp4',
|
||
progress: 44,
|
||
status: '正在截取帧',
|
||
});
|
||
});
|
||
|
||
expect(await screen.findByText('demo.mp4')).toBeInTheDocument();
|
||
expect(screen.getByText('44%')).toBeInTheDocument();
|
||
});
|
||
|
||
it('updates the websocket badge from connection status callbacks', async () => {
|
||
render(<Dashboard />);
|
||
|
||
await waitFor(() => expect(wsMock.progressWS.onStatus).toHaveBeenCalled());
|
||
expect(screen.getByText('WebSocket 断开')).toBeInTheDocument();
|
||
|
||
act(() => {
|
||
wsMock.state.connected = true;
|
||
wsMock.state.statusCallback?.('connected');
|
||
});
|
||
|
||
expect(screen.getByText('WebSocket 已连接')).toBeInTheDocument();
|
||
});
|
||
|
||
it('adds activity logs for complete and status messages', async () => {
|
||
render(<Dashboard />);
|
||
|
||
act(() => {
|
||
wsMock.state.callback?.({ type: 'status', message: 'Progress stream active' });
|
||
wsMock.state.callback?.({ type: 'complete', taskId: '1', filename: 'done.mp4' });
|
||
});
|
||
|
||
await waitFor(() => expect(screen.getByText('Progress stream active')).toBeInTheDocument());
|
||
expect(screen.getByText('解析完成: done.mp4')).toBeInTheDocument();
|
||
});
|
||
|
||
it('cancels, retries, and opens task failure details', async () => {
|
||
apiMock.getDashboardOverview.mockResolvedValueOnce({
|
||
summary: {
|
||
project_count: 1,
|
||
parsing_task_count: 1,
|
||
annotation_count: 0,
|
||
frame_count: 0,
|
||
template_count: 0,
|
||
system_load_percent: 5,
|
||
},
|
||
tasks: [
|
||
{
|
||
id: 'task-10',
|
||
task_id: 10,
|
||
project_id: 1,
|
||
name: 'running.mp4',
|
||
progress: 30,
|
||
status: '正在下载媒体文件',
|
||
raw_status: 'running',
|
||
error: null,
|
||
frame_count: 0,
|
||
updated_at: '2026-05-01T00:00:00Z',
|
||
},
|
||
{
|
||
id: 'task-11',
|
||
task_id: 11,
|
||
project_id: 1,
|
||
name: 'failed.mp4',
|
||
progress: 100,
|
||
status: '解析失败',
|
||
raw_status: 'failed',
|
||
error: 'ffmpeg failed',
|
||
frame_count: 0,
|
||
updated_at: '2026-05-01T00:01:00Z',
|
||
},
|
||
],
|
||
activity: [],
|
||
});
|
||
apiMock.cancelTask.mockResolvedValueOnce({
|
||
id: 10,
|
||
task_type: 'parse_video',
|
||
status: 'cancelled',
|
||
progress: 100,
|
||
message: '任务已取消',
|
||
project_id: 1,
|
||
error: 'Cancelled by user',
|
||
result: null,
|
||
payload: { source_type: 'video' },
|
||
created_at: 'created',
|
||
updated_at: 'updated',
|
||
});
|
||
apiMock.retryTask.mockResolvedValueOnce({
|
||
id: 12,
|
||
task_type: 'parse_video',
|
||
status: 'queued',
|
||
progress: 0,
|
||
message: '重试任务已入队(源任务 #11)',
|
||
project_id: 1,
|
||
error: null,
|
||
result: null,
|
||
payload: { source_type: 'video', retry_of: 11 },
|
||
created_at: 'created',
|
||
updated_at: 'updated',
|
||
});
|
||
apiMock.getTask.mockResolvedValueOnce({
|
||
id: 11,
|
||
task_type: 'parse_video',
|
||
status: 'failed',
|
||
progress: 100,
|
||
message: '解析失败',
|
||
project_id: 1,
|
||
celery_task_id: 'celery-11',
|
||
payload: { source_type: 'video' },
|
||
result: null,
|
||
error: 'ffmpeg failed',
|
||
created_at: 'created',
|
||
started_at: 'started',
|
||
finished_at: 'finished',
|
||
updated_at: 'updated',
|
||
});
|
||
|
||
render(<Dashboard />);
|
||
|
||
await screen.findByText('running.mp4');
|
||
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
||
await waitFor(() => expect(apiMock.cancelTask).toHaveBeenCalledWith(10));
|
||
|
||
fireEvent.click(screen.getAllByRole('button', { name: '详情' })[1]);
|
||
await waitFor(() => expect(apiMock.getTask).toHaveBeenCalledWith(11));
|
||
expect(await screen.findByText('任务详情 #11')).toBeInTheDocument();
|
||
expect(screen.getByText('ffmpeg failed')).toBeInTheDocument();
|
||
|
||
fireEvent.click(screen.getAllByRole('button', { name: '重试' })[1]);
|
||
await waitFor(() => expect(apiMock.retryTask).toHaveBeenCalledWith(11));
|
||
});
|
||
});
|