补充AI推理与手工绘制测试覆盖
- 新增 Vitest 配置和前端测试 setup,使 Docker 交付目录可直接运行组件测试 - 新增 ToolsPalette 测试,覆盖创建多边形、AI自动推理、打开 AI 智能分割、工具分组和画笔/橡皮擦尺寸控制 - 新增 CanvasArea 测试,覆盖创建多边形 Enter 完成、点击首节点闭合、Esc 取消临时点、画笔新建/合并、橡皮擦扣除和图像边界裁剪 - 复用并验证 AISegmentation 与 VideoWorkspace 测试,覆盖 AI 智能分割参数、模型不可用禁用、AI自动推理范围选择和多 seed 后台传播任务 - 更新软著功能验证与素材清单,补充 AI 智能分割、AI自动推理、创建多边形等功能的自动化测试映射
This commit is contained in:
178
src/test/setup.tsx
Normal file
178
src/test/setup.tsx
Normal 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',
|
||||
],
|
||||
}));
|
||||
30
src/test/storeTestUtils.ts
Normal file
30
src/test/storeTestUtils.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user