Files
Genarrative/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
T
kdletters 7417370ae9 修复画布任务列表服务端任务显示
任务列表只使用服务端返回的生成任务。

保留生成任务水合后的开始和结束时间。

同步任务列表与画布阶段视图测试。
2026-06-29 17:22:42 +08:00

365 lines
12 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { createRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
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 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(),
setAssetPublicShowcaseEnabled: vi.fn(),
toggleAssetSelected: vi.fn(),
addAssetLayer: 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 {
projectId: 'project-1',
projectTitle: '默认项目',
projectRenameValue: '默认项目',
isRenamingProject: false,
isProjectRenameSaving: false,
projectRenameError: null,
layers: [],
walletBalanceLabel: '0泥点',
isWalletBalanceLoading: false,
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(),
onOpenWallet: vi.fn(),
};
}
function createStageProps(): ImageCanvasStageViewProps {
return {
projectId: 'project-1',
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: [],
isTaskSidebarOpen: 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,
canUndo: false,
canRedo: false,
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(),
onActivateGenerationDialog: vi.fn(),
onFocusExternalTask: vi.fn(),
onToggleTaskSidebar: vi.fn(),
onCropExpandHandlePointerDown: vi.fn(),
onOpenQuickEditPanel: vi.fn(),
onOpenRedrawPanel: vi.fn(),
onOpenCropExpandPanel: vi.fn(),
onRemoveBackground: 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');
});
});