- 移除 Canvas 右下角旧清空遮罩和应用分类按钮,清空入口统一到左侧工具栏 - 清空遮罩优先作用于当前帧选中 mask,无选中时作用于当前帧全部 mask - 目标 mask 无传播链结果时直接清当前帧,有传播链结果时弹窗选择只清当前帧、清空传播所有帧或取消 - 保留布尔工具右下角合并/去除操作区,避免旧分类按钮误改整帧 - 更新 Canvas、工具栏、工作区测试,覆盖直接清空、传播链范围选择和取消路径 - 同步更新前端审计、需求冻结、设计冻结、测试计划和 AGENTS 说明
128 lines
5.6 KiB
TypeScript
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');
|
|
});
|
|
});
|