Files
Pre_Seg_Server/src/components/ToolsPalette.test.tsx
admin a22af5f7c8 统一工作区清空遮罩入口
- 移除 Canvas 右下角旧清空遮罩和应用分类按钮,清空入口统一到左侧工具栏

- 清空遮罩优先作用于当前帧选中 mask,无选中时作用于当前帧全部 mask

- 目标 mask 无传播链结果时直接清当前帧,有传播链结果时弹窗选择只清当前帧、清空传播所有帧或取消

- 保留布尔工具右下角合并/去除操作区,避免旧分类按钮误改整帧

- 更新 Canvas、工具栏、工作区测试,覆盖直接清空、传播链范围选择和取消路径

- 同步更新前端审计、需求冻结、设计冻结、测试计划和 AGENTS 说明
2026-05-03 21:06:03 +08:00

128 lines
5.6 KiB
TypeScript

import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useStore } from '../store/useStore';
import { resetStore } from '../test/storeTestUtils';
import { ToolsPalette } from './ToolsPalette';
describe('ToolsPalette', () => {
beforeEach(() => {
resetStore();
});
it('switches workspace editing tools without showing AI prompt or duplicate undo tools', () => {
const setActiveTool = vi.fn();
render(
<ToolsPalette
activeTool="move"
setActiveTool={setActiveTool}
/>,
);
fireEvent.click(screen.getByTitle('创建多边形 (P)'));
fireEvent.click(screen.getByTitle('调整多边形 (E)'));
fireEvent.click(screen.getByTitle('画笔 (B)'));
fireEvent.click(screen.getByTitle('橡皮擦 (X)'));
expect(setActiveTool).toHaveBeenNthCalledWith(1, 'create_polygon');
expect(setActiveTool).toHaveBeenNthCalledWith(2, 'edit_polygon');
expect(setActiveTool).toHaveBeenNthCalledWith(3, 'brush');
expect(setActiveTool).toHaveBeenNthCalledWith(4, 'eraser');
expect(screen.queryByTitle('正向选点 (SAM)')).not.toBeInTheDocument();
expect(screen.queryByTitle('反向选点 (SAM)')).not.toBeInTheDocument();
expect(screen.queryByTitle('边界框选 (SAM)')).not.toBeInTheDocument();
expect(screen.queryByTitle('撤销操作 (Ctrl+Z)')).not.toBeInTheDocument();
expect(screen.queryByTitle('重做操作 (Ctrl+Shift+Z)')).not.toBeInTheDocument();
expect(screen.queryByTitle('创建点 (C)')).not.toBeInTheDocument();
expect(screen.queryByTitle('创建线段 (L)')).not.toBeInTheDocument();
});
it('shows size controls for brush and eraser tools', () => {
const { rerender } = render(<ToolsPalette activeTool="brush" setActiveTool={vi.fn()} />);
const brushSize = screen.getByLabelText('画笔大小');
fireEvent.change(brushSize, { target: { value: '36' } });
expect(useStore.getState().brushSize).toBe(36);
rerender(<ToolsPalette activeTool="eraser" setActiveTool={vi.fn()} />);
const eraserSize = screen.getByLabelText('橡皮擦大小');
fireEvent.change(eraserSize, { target: { value: '48' } });
expect(useStore.getState().eraserSize).toBe(48);
});
it('places GT mask import after overlap removal with a distinct violet style', () => {
const onImportGtMask = vi.fn();
render(
<ToolsPalette
activeTool="move"
setActiveTool={vi.fn()}
onImportGtMask={onImportGtMask}
canImportGtMask
/>,
);
const overlapButton = screen.getByTitle('重叠区域去除 (-)');
const importButton = screen.getByTitle('导入 GT Mask');
fireEvent.click(importButton);
expect(onImportGtMask).toHaveBeenCalled();
expect(importButton).toHaveClass('bg-violet-500/10');
expect(overlapButton.compareDocumentPosition(importButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it('exposes clear mask action in the left toolbar', () => {
const onClearMasks = vi.fn();
render(<ToolsPalette activeTool="move" setActiveTool={vi.fn()} onClearMasks={onClearMasks} />);
fireEvent.click(screen.getByTitle('清空遮罩'));
expect(onClearMasks).toHaveBeenCalled();
});
it('separates drawing, editing, and external action tool groups', () => {
render(<ToolsPalette activeTool="move" setActiveTool={vi.fn()} canImportGtMask />);
const separators = screen.getAllByTestId('tool-group-separator');
const circleButton = screen.getByTitle('创建圆 (O)');
const brushButton = screen.getByTitle('画笔 (B)');
const removeButton = screen.getByTitle('重叠区域去除 (-)');
const clearButton = screen.getByTitle('清空遮罩');
const importButton = screen.getByTitle('导入 GT Mask');
expect(separators).toHaveLength(2);
expect(circleButton.compareDocumentPosition(separators[0]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(separators[0].compareDocumentPosition(brushButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(removeButton.compareDocumentPosition(separators[1]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(separators[1].compareDocumentPosition(clearButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(clearButton.compareDocumentPosition(importButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(separators[1].compareDocumentPosition(importButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
separators.forEach((separator) => {
expect(separator).toHaveClass('bg-white/15');
});
});
it('switches to SAM trigger and calls the AI navigation hook', () => {
const setActiveTool = vi.fn();
const onTriggerAI = vi.fn();
render(<ToolsPalette activeTool="move" setActiveTool={setActiveTool} onTriggerAI={onTriggerAI} />);
const aiButton = screen.getByTitle('打开 AI 智能分割');
expect(aiButton.querySelector('[data-testid="ai-segmentation-icon"]')).toBeInTheDocument();
fireEvent.click(aiButton);
expect(setActiveTool).toHaveBeenCalledWith('sam_trigger');
expect(onTriggerAI).toHaveBeenCalled();
});
it('uses compact vertically scrollable layout for smaller workspaces', () => {
const { container } = render(<ToolsPalette activeTool="move" setActiveTool={vi.fn()} />);
const palette = container.firstElementChild;
expect(palette).toHaveClass('w-14');
expect(palette).toHaveClass('overflow-y-auto');
expect(palette).toHaveClass('seg-scrollbar');
expect(palette?.firstElementChild).toHaveClass('w-12');
expect(screen.getByTitle('创建多边形 (P)')).toHaveClass('h-9');
expect(screen.getByTitle('打开 AI 智能分割')).toHaveClass('h-9');
});
});