/* @vitest-environment jsdom */ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import type { StageMinimapModel } from './ImageCanvasInteractionModel'; import { ImageCanvasPanelDockView } from './ImageCanvasPanelDockView'; function renderPanelDock( overrides: Partial[0]> = {}, ) { const props: Parameters[0] = { viewport: { x: 0, y: 0, scale: 1 }, canvasBackgroundColor: '#f8fafc', canvasBackgroundHexValue: '#f8fafc', canUndo: true, canRedo: true, isZoomMenuOpen: false, isBackgroundSettingsOpen: false, activeSidebarPanel: null, isAgentConversationEnabled: true, isAgentConversationOpen: false, isMinimapOpen: false, minimapModel: null, onFitLayers: vi.fn(), onUndoCanvasChange: vi.fn(), onRedoCanvasChange: vi.fn(), onUpdateScaleFromCenter: vi.fn(), onToggleZoomMenu: vi.fn(), onCloseZoomMenu: vi.fn(), onToggleBackgroundSettings: vi.fn(), onApplyCanvasBackgroundColor: vi.fn(), onCanvasBackgroundHexChange: vi.fn(), onToggleSidebarPanel: vi.fn(), onToggleAgentConversation: vi.fn(), onToggleMinimap: vi.fn(), onMinimapPointerDown: vi.fn(), ...overrides, }; render(); return props; } function mockElementRect( element: Element, rect: Pick, ) { Object.defineProperty(element, 'getBoundingClientRect', { configurable: true, value: () => ({ ...rect, right: rect.left + rect.width, bottom: rect.top + rect.height, x: rect.left, y: rect.top, toJSON: () => ({}), }), }); } describe('ImageCanvasPanelDockView', () => { it('renders panel dock actions and forwards common controls', () => { const props = renderPanelDock({ activeSidebarPanel: 'assets', isMinimapOpen: true, minimapModel: { bounds: { minX: 0, minY: 0, maxX: 100, maxY: 100 }, scale: 1, layers: [], viewport: { left: '10%', top: '12%', width: '20%', height: '24%' }, } satisfies StageMinimapModel, }); const toolbar = screen.getByRole('toolbar', { name: '画布面板入口' }); expect(toolbar.className).toContain( 'image-canvas-editor__panel-dock--sidebar-open', ); expect( within(toolbar) .getByRole('button', { name: '打开素材' }) .getAttribute('aria-pressed'), ).toBe('true'); expect(within(toolbar).getByRole('button', { name: '恢复' })).toBeTruthy(); expect(screen.getByRole('button', { name: '画布小地图' })).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: '重置画布视图' })); fireEvent.click(within(toolbar).getByRole('button', { name: '撤销' })); fireEvent.click(within(toolbar).getByRole('button', { name: '恢复' })); fireEvent.click(within(toolbar).getByRole('button', { name: '打开图层' })); fireEvent.click( within(toolbar).getByRole('button', { name: '画布 Agent' }), ); fireEvent.click( within(toolbar).getByRole('button', { name: '切换小地图' }), ); expect(props.onFitLayers).toHaveBeenCalledTimes(1); expect(props.onUndoCanvasChange).toHaveBeenCalledTimes(1); expect(props.onRedoCanvasChange).toHaveBeenCalledTimes(1); expect(props.onToggleSidebarPanel).toHaveBeenCalledWith('layers'); expect(props.onToggleAgentConversation).toHaveBeenCalledTimes(1); expect(props.onToggleMinimap).toHaveBeenCalledTimes(1); }); it('hides the Agent entry when the Agent sidebar is disabled', () => { renderPanelDock({ isAgentConversationEnabled: false }); expect(screen.queryByRole('button', { name: '画布 Agent' })).toBeNull(); }); it('keeps the background picker visible when the selected color is white', () => { const props = renderPanelDock({ canvasBackgroundColor: '#ffffff', canvasBackgroundHexValue: '#ffffff', isBackgroundSettingsOpen: true, }); const panel = screen.getByRole('dialog', { name: '画布背景设置' }); expect( screen .getByRole('button', { name: '画布背景色' }) .getAttribute('aria-expanded'), ).toBe('true'); const spectrumButton = within(panel).getByRole('button', { name: '画布背景色盘', }); const hueButton = within(panel).getByRole('button', { name: '画布背景色相', }); const spectrumHandle = spectrumButton.querySelector( '.image-canvas-editor__background-spectrum-handle', ) as HTMLElement; const hueHandle = hueButton.querySelector( '.image-canvas-editor__background-hue-handle', ) as HTMLElement; expect( spectrumButton.style.getPropertyValue( '--image-canvas-background-hue-color', ), ).toBe('#0080ff'); expect( spectrumHandle.style.getPropertyValue( '--image-canvas-background-spectrum-handle-left', ), ).toBe('clamp(0.42rem, 0%, calc(100% - 0.42rem))'); expect( spectrumHandle.style.getPropertyValue( '--image-canvas-background-spectrum-handle-top', ), ).toBe('clamp(0.42rem, 0%, calc(100% - 0.42rem))'); expect( hueHandle.style.getPropertyValue( '--image-canvas-background-hue-handle-left', ), ).toBe( 'clamp(0.39rem, 58.333333333333336%, calc(100% - 0.39rem))', ); mockElementRect(hueButton, { left: 0, top: 0, width: 100, height: 20, }); fireEvent.pointerDown(hueButton, { pointerId: 1, buttons: 1, clientX: 20, clientY: 10, }); expect(props.onApplyCanvasBackgroundColor).toHaveBeenCalledWith('#ffffff'); }); it('renders zoom and background settings with callback wiring', () => { const props = renderPanelDock({ viewport: { x: 0, y: 0, scale: 0.5 }, canvasBackgroundColor: '#ff0000', canvasBackgroundHexValue: '#ff0000', isZoomMenuOpen: true, isBackgroundSettingsOpen: true, }); expect( screen.getByRole('button', { name: '当前缩放比例 100%' }), ).toBeTruthy(); fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' })); fireEvent.click(screen.getByRole('menuitem', { name: '缩放至100%' })); fireEvent.click(screen.getByRole('menuitem', { name: '缩放至200%' })); fireEvent.click(screen.getByRole('menuitem', { name: '显示画布所有元素' })); expect(props.onUpdateScaleFromCenter).toHaveBeenCalledWith(0.25); expect(props.onUpdateScaleFromCenter).toHaveBeenCalledWith(0.5); expect(props.onUpdateScaleFromCenter).toHaveBeenCalledWith(1); expect(props.onFitLayers).toHaveBeenCalledTimes(1); expect(props.onCloseZoomMenu).toHaveBeenCalledTimes(4); const panel = screen.getByRole('dialog', { name: '画布背景设置' }); expect(panel.querySelector('input[type="color"]')).toBeNull(); fireEvent.click(within(panel).getByRole('button', { name: '暖灰' })); const spectrumButton = within(panel).getByRole('button', { name: '画布背景色盘', }); mockElementRect(spectrumButton, { left: 0, top: 0, width: 100, height: 100, }); fireEvent.pointerDown(spectrumButton, { pointerId: 1, buttons: 1, clientX: 50, clientY: 50, }); fireEvent.change(within(panel).getByLabelText('画布背景十六进制颜色'), { target: { value: '#abc' }, }); fireEvent.click(within(panel).getByRole('button', { name: '恢复默认' })); fireEvent.click( within(panel).getByRole('button', { name: '关闭画布背景设置' }), ); expect(props.onApplyCanvasBackgroundColor).toHaveBeenCalledWith('#f3f0ea'); expect(props.onApplyCanvasBackgroundColor).toHaveBeenCalledWith('#804040'); expect(props.onCanvasBackgroundHexChange).toHaveBeenCalledWith('#abc'); expect(props.onApplyCanvasBackgroundColor).toHaveBeenCalledWith('#f8fafc'); expect(props.onToggleBackgroundSettings).toHaveBeenCalledTimes(1); }); });