Files
Genarrative/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
T
JenkenB 75e1ac93db Refactor UI asset extraction overlay and handlers
Remove explicit cancel callback and centralize dismissal: clicking the canvas now dismisses active UI extraction (handled in ImageCanvasEditorView) instead of relying on an onCancel prop. Simplify ImageCanvasStageView and ImageCanvasUiAssetExtractionOverlayView APIs by removing onCancel and related UI. Improve preview bounds and rendering: add normalization to mark bounds, clamp to source dimensions, and render crop previews using an absolutely-positioned <img> for accurate, coordinate-based previews. Update styles for toolbar active states and preview image layout, and reduce SVG stroke width. Update tests and docs to reflect new behaviors and UI wording (tool highlight, accurate crop preview, and dismissal behavior).
2026-06-22 21:37:39 +08:00

325 lines
10 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(),
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 {
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: [],
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,
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(),
onCropExpandHandlePointerDown: vi.fn(),
onOpenQuickEditPanel: vi.fn(),
onOpenRedrawPanel: vi.fn(),
onOpenCropExpandPanel: vi.fn(),
onRemoveBackground: vi.fn(),
onExtractUiDesignAssets: vi.fn(),
onUiAssetExtractionToolChange: vi.fn(),
onUiAssetExtractionPointerStart: vi.fn(),
onUiAssetExtractionPointerMove: vi.fn(),
onUiAssetExtractionPointerEnd: 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('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,
marks: [],
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');
});
});