/* @vitest-environment jsdom */ import { fireEvent, render as testingLibraryRender, screen, within, } from '@testing-library/react'; import { createRef, type ReactElement, type ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider'; import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import type { ImageCanvasMetadataModalViewProps } from './ImageCanvasMetadataModalView'; import type { ImageCanvasSidebarViewProps } from './ImageCanvasSidebarView'; import type { ImageCanvasStageViewProps } from './ImageCanvasStageView'; import type { ImageCanvasTopbarViewProps } from './ImageCanvasTopbarView'; function render(ui: ReactElement) { return testingLibraryRender(ui, { wrapper: ({ children }: { children: ReactNode }) => ( {children} ), }); } function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-1', resourceId: 'resource-1', title: '测试图片', src: 'data:image/png;base64,layer', x: 0, y: 0, width: 320, height: 240, originalWidth: 320, originalHeight: 240, zIndex: 1, sourceType: 'uploaded', ...overrides, }; } function createSidebarProps(): ImageCanvasSidebarViewProps { return { activeSidebarPanel: null, assetListRef: createRef(), assetPointerDragRef: { current: null }, suppressAssetClickRef: { current: false }, assets: [], groupedAssets: [], assetFolders: [], layers: [], selectedLayerId: null, selectedLayerIds: [], isAssetSelectionMode: false, selectedAssetIds: new Set(), assetMoveDropFolderId: null, pinnedAssetMoveFolderId: null, creatingFolder: false, newFolderName: '', renamingFolder: null, renamingAsset: null, allSelectableAssetsSelected: false, assetMarquee: null, setIsAssetSelectionMode: vi.fn(), setCreatingFolder: vi.fn(), setNewFolderName: vi.fn(), setRenamingFolder: vi.fn(), setRenamingAsset: vi.fn(), setActiveUploadFolderId: vi.fn(), setUploadDropTarget: vi.fn(), setAssetPointerDrag: vi.fn(), setSelectedAssetIds: vi.fn(), setImageContextMenu: vi.fn(), setContextMenu: vi.fn(), onAssetMarqueePointerDown: vi.fn(), onAssetMarqueePointerMove: vi.fn(), onAssetMarqueePointerUp: vi.fn(), updateAssetMoveDropFolder: vi.fn(), addUploadedFiles: vi.fn(), requestUpload: vi.fn(), moveAssetToFolder: vi.fn(), commitNewAssetFolder: vi.fn(), toggleAssetFolder: vi.fn(), startRenamingFolder: vi.fn(), commitFolderRename: vi.fn(), deleteAssetFolder: vi.fn(), startRenamingAsset: vi.fn(), commitAssetRename: vi.fn(), deleteUploadedAsset: vi.fn(), submitAssetToShowcase: vi.fn(), toggleAssetSelected: vi.fn(), addAssetLayer: vi.fn(), onDownloadAsset: vi.fn(), toggleAllAssetsSelected: vi.fn(), deleteSelectedAssets: vi.fn(), closeAssetSelectionMode: vi.fn(), groupSelectedLayers: vi.fn(), selectSingleLayer: vi.fn(), resolveContextMenuPosition: vi.fn(() => ({ x: 0, y: 0 })), getCanvasPointFromClient: vi.fn(() => ({ x: 0, y: 0 })), }; } function createTopbarProps(): ImageCanvasTopbarViewProps { return { projectTitle: '默认项目', projectRenameValue: '默认项目', isRenamingProject: false, isProjectRenameSaving: false, projectRenameError: null, layers: [], walletBalance: 0, walletBreakdown: null, isWalletBalanceLoading: false, isWalletDetailsLoading: false, walletDetailsError: null, currentUser: null, assetExportStatus: null, isExportingAssets: false, setProjectRenameValue: vi.fn(), startProjectRename: vi.fn(), cancelProjectRename: vi.fn(), submitProjectRename: vi.fn(), resetProjectRenameError: vi.fn(), exportCanvasAssets: vi.fn(), onOpenShortcuts: vi.fn(), onRequestWalletDetails: vi.fn(), onRecharge: vi.fn(), onOpenWalletLedger: vi.fn(), onOpenAccount: vi.fn(), }; } function createStageProps(): ImageCanvasStageViewProps { return { canvasViewportRef: createRef(), specToolWrapRef: createRef(), musicToolWrapRef: createRef(), publicationToolWrapRef: createRef(), isPanning: false, effectiveTool: 'select', canvasBackgroundColor: '#f8fafc', canvasBackgroundHexValue: '#f8fafc', viewport: { x: 0, y: 0, scale: 1 }, snapGuide: null, layers: [], selectedLayer: null, selectedLayerIds: [], hoveredLayerId: null, canvasMarquee: null, canvasGenerationDialogs: [], editorAgentAssets: [], taskListRefreshKey: 0, isTaskSidebarOpen: false, isAgentConversationEnabled: false, isAgentConversationOpen: false, generateDialog: null, cropExpandPanel: null, cropExpandSourceLayer: null, uiAssetExtractionState: null, uiAssetExtractionSourceLayer: null, quickEditSelectionState: null, quickEditSelectionSourceLayer: null, generationComposerStyle: null, selectedToolbarStyle: null, uploadDropTarget: null, contextMenu: null, canvasClipboard: null, imageContextMenu: null, imageContextMenuLayer: null, contextMenuLayer: null, contextShouldShowLayer: false, contextShouldUnlockLayer: false, canCopyContextLayers: true, isCanvasClipboardCopyBlocked: false, canUndo: false, canRedo: false, historyFeedback: null, isZoomMenuOpen: false, isBackgroundSettingsOpen: false, activeSidebarPanel: null, isMinimapOpen: false, minimapModel: null, onCanvasPointerDown: vi.fn(), onCanvasPointerMove: vi.fn(), onCanvasPointerUp: vi.fn(), onCanvasDragOver: vi.fn(), onCanvasDragLeave: vi.fn(), onCanvasDrop: vi.fn(), onCanvasContextMenu: vi.fn(), onLayerPointerDown: vi.fn(), onLayerClick: vi.fn(), onSelectLayer: vi.fn(), onLayerContextMenu: vi.fn(), onLayerMouseEnter: vi.fn(), onLayerMouseLeave: vi.fn(), onOpenLayerMetadata: vi.fn(), onUpdateLayerAssetKind: vi.fn(), onGenerationFramePointerDown: vi.fn(), onGenerationFrameContextMenu: vi.fn(), onActivateGenerationDialog: vi.fn(), onFocusExternalTask: vi.fn(), onToggleTaskSidebar: vi.fn(), onToggleAgentConversation: vi.fn(), onCropExpandHandlePointerDown: vi.fn(), onOpenQuickEditPanel: vi.fn(), onOpenRedrawPanel: vi.fn(), onOpenCropExpandPanel: vi.fn(), onRemoveBackground: vi.fn(), onSplitIconSpritesheet: vi.fn(), onExtractUiDesignAssets: vi.fn(), onUiAssetExtractionToolChange: vi.fn(), onUiAssetExtractionModelChange: vi.fn(), onUiAssetExtractionPointerStart: vi.fn(), onUiAssetExtractionPointerMove: vi.fn(), onUiAssetExtractionPointerEnd: vi.fn(), onRequestUiAssetExtractionReferenceUpload: vi.fn(), onRemoveUiAssetExtractionReference: vi.fn(), onSubmitUiAssetExtraction: vi.fn(), onQuickEditSelectionToolChange: vi.fn(), onQuickEditSelectionPointerStart: vi.fn(), onQuickEditSelectionPointerMove: vi.fn(), onQuickEditSelectionPointerEnd: vi.fn(), onOpenCharacterAnimationPanel: vi.fn(), onDownloadLayer: vi.fn(), onPasteCanvasClipboard: vi.fn(), onCopyContextLayers: vi.fn(), onDuplicateContextLayers: vi.fn(), onMoveContextLayers: vi.fn(), onGroupContextLayers: vi.fn(), onUngroupContextLayers: vi.fn(), onToggleContextLayerVisibility: vi.fn(), onToggleContextLayerLock: vi.fn(), onFlipContextLayers: vi.fn(), onExportContextLayer: vi.fn(), onDeleteContextLayers: vi.fn(), onDeleteLayerById: vi.fn(), onCloseContextMenu: vi.fn(), onCloseImageContextMenu: vi.fn(), onUpdateScaleFromCenter: vi.fn(), onFitLayers: vi.fn(), onUndoCanvasChange: vi.fn(), onRedoCanvasChange: vi.fn(), onToggleZoomMenu: vi.fn(), onCloseZoomMenu: vi.fn(), onToggleBackgroundSettings: vi.fn(), onApplyCanvasBackgroundColor: vi.fn(), onCanvasBackgroundHexChange: vi.fn(), onToggleSidebarPanel: vi.fn(), onToggleMinimap: vi.fn(), onMinimapPointerDown: vi.fn(), onSwitchTool: vi.fn(), onOpenToolOptions: vi.fn(), onCloseToolOptions: vi.fn(), }; } function createMetadataProps( layer: CanvasLayer | null = null, ): ImageCanvasMetadataModalViewProps { return { layer, onClose: vi.fn(), }; } describe('ImageCanvasEditorShellView', () => { it('composes upload input, topbar, stage and metadata modal', () => { const handleUploadInputChange = vi.fn(); render( ()} uploadInputRef={createRef()} onUploadInputChange={handleUploadInputChange} assetDragPreview={null} sidebarProps={createSidebarProps()} topbarProps={createTopbarProps()} stageProps={createStageProps()} metadataProps={createMetadataProps(createLayer())} />, ); expect(screen.getByRole('region', { name: '图片画布编辑器' })).toBeTruthy(); expect(screen.getByRole('heading', { name: '默认项目' })).toBeTruthy(); expect(screen.getByLabelText('画布工作区')).toBeTruthy(); expect(screen.getByRole('toolbar', { name: 'AI画布工具栏' })).toBeTruthy(); expect(screen.getByRole('dialog', { name: '图片信息' })).toBeTruthy(); const uploadInput = screen.getByLabelText( '上传图片文件', ) as HTMLInputElement; fireEvent.change(uploadInput); expect(uploadInput.multiple).toBe(true); expect(uploadInput.accept).toBe('image/*'); expect(handleUploadInputChange).toHaveBeenCalledTimes(1); }); it('renders the asset drag preview with sanitized coordinates', () => { render( ()} uploadInputRef={createRef()} onUploadInputChange={vi.fn()} assetDragPreview={{ x: Number.NaN, y: 48, label: '拖拽素材' }} sidebarProps={createSidebarProps()} topbarProps={createTopbarProps()} stageProps={createStageProps()} metadataProps={createMetadataProps()} />, ); const preview = screen.getByText('拖拽素材'); expect(preview.className).toBe('image-canvas-editor__asset-drag-preview'); expect((preview as HTMLElement).style.left).toBe('0px'); expect((preview as HTMLElement).style.top).toBe('48px'); }); it('keeps the native context menu available inside metadata text', () => { render( ()} uploadInputRef={createRef()} onUploadInputChange={vi.fn()} assetDragPreview={null} sidebarProps={createSidebarProps()} topbarProps={createTopbarProps()} stageProps={createStageProps()} metadataProps={createMetadataProps(createLayer())} />, ); const editor = screen.getByRole('region', { name: '图片画布编辑器' }); const metadataText = screen.getByText('上传图片'); const metadataContextMenuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, }); metadataText.dispatchEvent(metadataContextMenuEvent); expect(metadataContextMenuEvent.defaultPrevented).toBe(false); const editorContextMenuEvent = new MouseEvent('contextmenu', { bubbles: true, cancelable: true, }); editor.dispatchEvent(editorContextMenuEvent); expect(editorContextMenuEvent.defaultPrevented).toBe(true); }); it('renders quick edit selection tools through the stage props', () => { const layer = createLayer(); const handleQuickEditToolChange = vi.fn(); const stageProps = { ...createStageProps(), layers: [layer], quickEditSelectionSourceLayer: layer, quickEditSelectionState: { sourceLayerId: layer.id, tool: 'rect' as const, model: 'gemini-3.1-flash-image-preview', marks: [], references: [], draftMark: null, status: 'idle' as const, }, onQuickEditSelectionToolChange: handleQuickEditToolChange, }; render( ()} uploadInputRef={createRef()} onUploadInputChange={vi.fn()} assetDragPreview={null} sidebarProps={createSidebarProps()} topbarProps={createTopbarProps()} stageProps={stageProps} metadataProps={createMetadataProps()} />, ); const toolbar = screen.getByRole('toolbar', { name: '快速编辑框选工具', }); fireEvent.click(within(toolbar).getByRole('button', { name: '椭圆框选' })); expect(handleQuickEditToolChange).toHaveBeenCalledWith('ellipse'); }); });