/* @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(); 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(); 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(); 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(); 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(); 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(); 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(); vi.advanceTimersByTime(120); mocapMock.command = { actions: [], leftHand: null, rightHand: null, }; rerender(); 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(); 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(); 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(); 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(); expect(cursor.style.left).toBe('72%'); expect(cursor.style.top).toBe('34%'); });