Files
Genarrative/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
T
kdletters 7f18f7ce04
Project CI / Frontend tests (push) Failing after 21s
Project CI / Repository checks (push) Failing after 43s
Project CI / Backend tests (push) Successful in 3m44s
Project CI / Native shell tests (push) Successful in 10m42s
修复画布副本持久化与素材类型覆盖
复制布局复用项目资源并区分资源登记状态
新增图层素材类型覆盖并保持资源默认类型
补齐结构化存储迁移、绑定与权威类型校验
同步前后端回归测试和画布持久化文档
2026-08-04 18:06:15 +08:00

400 lines
13 KiB
TypeScript

/* @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 }) => (
<ImageCanvasActionsProvider
focusResource={vi.fn()}
refreshCanvas={vi.fn()}
>
{children}
</ImageCanvasActionsProvider>
),
});
}
function createLayer(overrides: Partial<CanvasLayer> = {}): 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<HTMLDivElement>(),
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<HTMLDivElement>(),
specToolWrapRef: createRef<HTMLSpanElement>(),
musicToolWrapRef: createRef<HTMLSpanElement>(),
publicationToolWrapRef: createRef<HTMLSpanElement>(),
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(
<ImageCanvasEditorShellView
editorRootRef={createRef<HTMLElement>()}
uploadInputRef={createRef<HTMLInputElement>()}
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(
<ImageCanvasEditorShellView
editorRootRef={createRef<HTMLElement>()}
uploadInputRef={createRef<HTMLInputElement>()}
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(
<ImageCanvasEditorShellView
editorRootRef={createRef<HTMLElement>()}
uploadInputRef={createRef<HTMLInputElement>()}
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(
<ImageCanvasEditorShellView
editorRootRef={createRef<HTMLElement>()}
uploadInputRef={createRef<HTMLInputElement>()}
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');
});
});