247 lines
7.3 KiB
TypeScript
247 lines
7.3 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import { BabyLoveDrawingRuntimeShell } from './BabyLoveDrawingRuntimeShell';
|
|
|
|
const saveBabyLoveDrawingMock = vi.fn();
|
|
const createBabyLoveDrawingMagicImageMock = vi.fn();
|
|
|
|
const mocapMock = vi.hoisted(() => ({
|
|
command: null as null | {
|
|
actions: string[];
|
|
leftHand?: {
|
|
x: number;
|
|
y: number;
|
|
state: 'open_palm' | 'grab' | 'unknown';
|
|
side: 'left';
|
|
} | null;
|
|
rightHand?: {
|
|
x: number;
|
|
y: number;
|
|
state: 'open_palm' | 'grab' | 'unknown';
|
|
side: 'right';
|
|
} | null;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../services/useMocapInput', () => ({
|
|
useMocapInput: () => ({
|
|
status: 'idle',
|
|
latestCommand: mocapMock.command,
|
|
rawPacketPreview: null,
|
|
error: null,
|
|
}),
|
|
}));
|
|
|
|
vi.mock('../../services/edutainment-baby-drawing', () => ({
|
|
createBabyLoveDrawingMagicImage: (...args: unknown[]) =>
|
|
createBabyLoveDrawingMagicImageMock(...args),
|
|
saveBabyLoveDrawing: (...args: unknown[]) => saveBabyLoveDrawingMock(...args),
|
|
}));
|
|
|
|
function installCanvasMock() {
|
|
const context = {
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
beginPath: vi.fn(),
|
|
moveTo: vi.fn(),
|
|
lineTo: vi.fn(),
|
|
stroke: vi.fn(),
|
|
fillRect: vi.fn(),
|
|
drawImage: vi.fn(),
|
|
set fillStyle(_value: string) {},
|
|
set strokeStyle(_value: string) {},
|
|
set lineWidth(_value: number) {},
|
|
set lineCap(_value: CanvasLineCap) {},
|
|
set lineJoin(_value: CanvasLineJoin) {},
|
|
set globalCompositeOperation(_value: GlobalCompositeOperation) {},
|
|
} as unknown as CanvasRenderingContext2D;
|
|
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context);
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockReturnValue(
|
|
'data:image/png;base64,original',
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
installCanvasMock();
|
|
mocapMock.command = null;
|
|
saveBabyLoveDrawingMock.mockReturnValue({
|
|
record: {
|
|
drawingId: 'baby-love-drawing-local-1',
|
|
templateId: 'baby-love-drawing',
|
|
templateName: '宝贝爱画',
|
|
originalImageSrc: 'data:image/png;base64,original',
|
|
magicImageSrc: null,
|
|
strokeTrace: [],
|
|
saveMode: 'original-only',
|
|
themeTags: ['寓教于乐', '宝贝爱画'],
|
|
createdAt: '2026-05-13T08:00:00.000Z',
|
|
updatedAt: '2026-05-13T08:00:00.000Z',
|
|
},
|
|
});
|
|
createBabyLoveDrawingMagicImageMock.mockResolvedValue({
|
|
magicImageSrc: 'data:image/png;base64,magic',
|
|
generationProvider: 'vector-engine-gpt-image-2',
|
|
prompt: '绘本风格',
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
test('renders drawing board, seven colors and tool buttons', () => {
|
|
render(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
expect(screen.getByTestId('baby-love-drawing-runtime')).toBeTruthy();
|
|
expect(screen.getByLabelText('画板')).toBeTruthy();
|
|
expect(screen.getByLabelText('红')).toBeTruthy();
|
|
expect(screen.getByLabelText('紫')).toBeTruthy();
|
|
expect(screen.getAllByRole('button')).toHaveLength(11);
|
|
expect(screen.getByLabelText('画笔')).toBeTruthy();
|
|
expect(screen.getByLabelText('橡皮')).toBeTruthy();
|
|
});
|
|
|
|
test('finish then save stores original drawing in local demo service', () => {
|
|
render(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '完成' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
expect(saveBabyLoveDrawingMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
originalImageSrc: 'data:image/png;base64,original',
|
|
magicImageSrc: null,
|
|
}),
|
|
);
|
|
expect(screen.getByText('已保存')).toBeTruthy();
|
|
});
|
|
|
|
test('back button calls onBack callback', () => {
|
|
const onBack = vi.fn();
|
|
render(<BabyLoveDrawingRuntimeShell onBack={onBack} />);
|
|
|
|
fireEvent.click(screen.getByLabelText('返回'));
|
|
|
|
expect(onBack).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('mocap camera-left hand drives the player right hand brush cursor', () => {
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: { x: 0.72, y: 0.34, state: 'open_palm', side: 'left' },
|
|
rightHand: null,
|
|
};
|
|
const { container, rerender } = render(<BabyLoveDrawingRuntimeShell />);
|
|
const cursor = container.querySelector(
|
|
'.baby-love-drawing-runtime__cursor',
|
|
) as HTMLElement;
|
|
|
|
expect(cursor.style.left).toBe('72%');
|
|
expect(cursor.style.top).toBe('34%');
|
|
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: null,
|
|
rightHand: { x: 0.18, y: 0.82, state: 'grab', side: 'right' },
|
|
};
|
|
rerender(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
expect(cursor.style.left).toBe('72%');
|
|
expect(cursor.style.top).toBe('34%');
|
|
});
|
|
|
|
test('mocap camera-right hand renders the player left hand color indicator', () => {
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: null,
|
|
rightHand: { x: 0.16, y: 0.42, state: 'open_palm', side: 'right' },
|
|
};
|
|
const { container } = render(<BabyLoveDrawingRuntimeShell />);
|
|
const indicator = container.querySelector(
|
|
'.baby-love-drawing-runtime__left-hand-indicator',
|
|
) as HTMLElement;
|
|
|
|
expect(indicator).toBeTruthy();
|
|
expect(indicator.style.left).toBe('16%');
|
|
expect(indicator.style.top).toBe('42%');
|
|
});
|
|
|
|
test('left hand indicator stays visible through brief mocap hand loss', () => {
|
|
vi.useFakeTimers();
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: null,
|
|
rightHand: { x: 0.16, y: 0.42, state: 'open_palm', side: 'right' },
|
|
};
|
|
const { container, rerender } = render(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
vi.advanceTimersByTime(120);
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: null,
|
|
rightHand: null,
|
|
};
|
|
rerender(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
const indicator = container.querySelector(
|
|
'.baby-love-drawing-runtime__left-hand-indicator',
|
|
) as HTMLElement;
|
|
expect(indicator).toBeTruthy();
|
|
expect(indicator.style.left).toBe('16%');
|
|
expect(indicator.style.top).toBe('42%');
|
|
});
|
|
|
|
test('player left hand never takes over the right hand brush cursor', () => {
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: { x: 0.68, y: 0.32, state: 'open_palm', side: 'left' },
|
|
rightHand: { x: 0.18, y: 0.78, state: 'open_palm', side: 'right' },
|
|
};
|
|
const { container, rerender } = render(<BabyLoveDrawingRuntimeShell />);
|
|
const cursor = container.querySelector(
|
|
'.baby-love-drawing-runtime__cursor',
|
|
) as HTMLElement;
|
|
|
|
expect(cursor.style.left).toBe('68%');
|
|
expect(cursor.style.top).toBe('32%');
|
|
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: null,
|
|
rightHand: { x: 0.7, y: 0.3, state: 'grab', side: 'right' },
|
|
};
|
|
rerender(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
expect(cursor.style.left).toBe('68%');
|
|
expect(cursor.style.top).toBe('32%');
|
|
});
|
|
|
|
test('large camera-left jump is rejected to prevent left hand stealing brush', () => {
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: { x: 0.72, y: 0.34, state: 'open_palm', side: 'left' },
|
|
rightHand: null,
|
|
};
|
|
const { container, rerender } = render(<BabyLoveDrawingRuntimeShell />);
|
|
const cursor = container.querySelector(
|
|
'.baby-love-drawing-runtime__cursor',
|
|
) as HTMLElement;
|
|
|
|
expect(cursor.style.left).toBe('72%');
|
|
expect(cursor.style.top).toBe('34%');
|
|
|
|
mocapMock.command = {
|
|
actions: [],
|
|
leftHand: { x: 0.16, y: 0.82, state: 'grab', side: 'left' },
|
|
rightHand: null,
|
|
};
|
|
rerender(<BabyLoveDrawingRuntimeShell />);
|
|
|
|
expect(cursor.style.left).toBe('72%');
|
|
expect(cursor.style.top).toBe('34%');
|
|
});
|