补充AI推理与手工绘制测试覆盖

- 新增 Vitest 配置和前端测试 setup,使 Docker 交付目录可直接运行组件测试
- 新增 ToolsPalette 测试,覆盖创建多边形、AI自动推理、打开 AI 智能分割、工具分组和画笔/橡皮擦尺寸控制
- 新增 CanvasArea 测试,覆盖创建多边形 Enter 完成、点击首节点闭合、Esc 取消临时点、画笔新建/合并、橡皮擦扣除和图像边界裁剪
- 复用并验证 AISegmentation 与 VideoWorkspace 测试,覆盖 AI 智能分割参数、模型不可用禁用、AI自动推理范围选择和多 seed 后台传播任务
- 更新软著功能验证与素材清单,补充 AI 智能分割、AI自动推理、创建多边形等功能的自动化测试映射
This commit is contained in:
2026-05-08 01:55:06 +08:00
parent 09f6137a8f
commit d369674906
6 changed files with 2748 additions and 0 deletions

178
src/test/setup.tsx Normal file
View File

@@ -0,0 +1,178 @@
import React from 'react';
import { afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
afterEach(() => {
cleanup();
localStorage.clear();
});
vi.stubGlobal('alert', vi.fn());
vi.stubGlobal('confirm', vi.fn(() => true));
URL.createObjectURL = vi.fn(() => 'blob:mock-url');
URL.revokeObjectURL = vi.fn();
HTMLAnchorElement.prototype.click = vi.fn();
function makeStageEvent(x = 120, y = 80) {
const stage = {
getPointerPosition: () => ({ x, y }),
getRelativePointerPosition: () => ({ x, y }),
scaleX: () => 1,
x: () => 0,
y: () => 0,
};
return {
evt: { preventDefault: vi.fn(), deltaY: -1 },
target: {
getStage: () => stage,
},
};
}
vi.mock('react-konva', () => ({
Stage: ({ children, onClick, onMouseDown, onMouseUp, onMouseMove, onWheel, onDragEnd, scaleX, scaleY, x, y, width, height }: any) => {
const coords = (event: React.MouseEvent<HTMLDivElement>, fallbackX: number, fallbackY: number) => ({
x: event.clientX || fallbackX,
y: event.clientY || fallbackY,
});
return (
<div
data-testid="konva-stage"
data-has-drag-end={Boolean(onDragEnd)}
data-scale-x={scaleX}
data-scale-y={scaleY}
data-x={x}
data-y={y}
data-width={width}
data-height={height}
onClick={(event) => {
const point = coords(event, 120, 80);
onClick?.(makeStageEvent(point.x, point.y));
}}
onMouseDown={(event) => {
const point = coords(event, 120, 80);
onMouseDown?.(makeStageEvent(point.x, point.y));
}}
onMouseUp={(event) => {
const point = coords(event, 260, 200);
onMouseUp?.(makeStageEvent(point.x, point.y));
}}
onMouseMove={(event) => {
const point = coords(event, 180, 120);
onMouseMove?.(makeStageEvent(point.x, point.y));
}}
onWheel={() => onWheel?.(makeStageEvent())}
onDragEnd={(event) => {
const stageTarget: any = {
x: () => event.clientX || 0,
y: () => event.clientY || 0,
};
stageTarget.getStage = () => stageTarget;
const childTarget = {
x: () => event.clientX || 0,
y: () => event.clientY || 0,
getStage: () => stageTarget,
};
onDragEnd?.({
target: event.target === event.currentTarget ? stageTarget : childTarget,
});
}}
>
{children}
</div>
);
},
Layer: ({ children }: any) => <div data-testid="konva-layer">{children}</div>,
Group: ({ children, opacity }: any) => <div data-testid="konva-group" data-opacity={opacity}>{children}</div>,
Image: ({ image }: any) => <img data-testid="konva-image" alt="" src={image?.src || ''} />,
Circle: (props: any) => (
<span
data-testid="konva-circle"
data-fill={props.fill}
data-x={props.x}
data-y={props.y}
onClick={(event) => {
const point = {
x: event.clientX || 120,
y: event.clientY || 80,
};
const konvaEvent = { ...makeStageEvent(point.x, point.y), cancelBubble: false };
props.onClick?.(konvaEvent);
if (konvaEvent.cancelBubble) event.stopPropagation();
}}
onMouseDown={(event) => {
const point = {
x: event.clientX || props.x || 120,
y: event.clientY || props.y || 80,
};
const konvaEvent = { ...makeStageEvent(point.x, point.y), cancelBubble: false };
props.onMouseDown?.(konvaEvent);
props.onDragStart?.(konvaEvent);
if (konvaEvent.cancelBubble) event.stopPropagation();
}}
onMouseMove={(event) => props.onDragMove?.({
target: {
x: () => event.clientX || props.x || 0,
y: () => event.clientY || props.y || 0,
},
})}
onMouseUp={(event: React.MouseEvent<HTMLSpanElement>) => props.onDragEnd?.({
target: {
x: () => event.clientX || props.x || 0,
y: () => event.clientY || props.y || 0,
},
})}
onDragEnd={(event: React.DragEvent<HTMLSpanElement>) => props.onDragEnd?.({
target: {
x: () => event.clientX || props.x || 0,
y: () => event.clientY || props.y || 0,
},
})}
/>
),
Rect: (props: any) => <span data-testid="konva-rect" data-width={props.width} />,
Path: (props: any) => (
<span
data-testid="konva-path"
data-path={props.data}
data-fill={props.fill}
data-stroke={props.stroke}
data-stroke-width={props.strokeWidth}
data-dash={props.dash?.join(',') || ''}
data-fill-rule={props.fillRule}
onClick={(event) => {
const point = {
x: event.clientX || 120,
y: event.clientY || 80,
};
const konvaEvent = { ...makeStageEvent(point.x, point.y), cancelBubble: false };
props.onClick?.(konvaEvent);
if (konvaEvent.cancelBubble) event.stopPropagation();
}}
onDoubleClick={(event) => {
const point = {
x: event.clientX || 120,
y: event.clientY || 80,
};
const konvaEvent = { ...makeStageEvent(point.x, point.y), cancelBubble: false };
props.onDblClick?.(konvaEvent);
if (konvaEvent.cancelBubble) event.stopPropagation();
}}
/>
),
}));
vi.mock('use-image', () => ({
default: (src: string) => [
{
src,
width: 640,
height: 360,
naturalWidth: 640,
naturalHeight: 360,
},
'loaded',
],
}));

View File

@@ -0,0 +1,30 @@
import { DEFAULT_AI_MODEL_ID, DEFAULT_BRUSH_SIZE, DEFAULT_ERASER_SIZE, useStore } from '../store/useStore';
export function resetStore() {
useStore.setState({
isAuthenticated: false,
token: null,
currentUser: null,
projects: [],
currentProject: null,
activeModule: 'dashboard',
activeTool: 'move',
aiModel: DEFAULT_AI_MODEL_ID,
frames: [],
currentFrameIndex: 0,
annotations: [],
masks: [],
selectedMaskIds: [],
maskPreviewOpacity: 50,
brushSize: DEFAULT_BRUSH_SIZE,
eraserSize: DEFAULT_ERASER_SIZE,
maskHistory: [],
maskFuture: [],
templates: [],
activeTemplateId: null,
activeClassId: null,
activeClass: null,
isLoading: false,
error: null,
});
}