7417370ae9
任务列表只使用服务端返回的生成任务。 保留生成任务水合后的开始和结束时间。 同步任务列表与画布阶段视图测试。
365 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|