75e1ac93db
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).
325 lines
10 KiB
TypeScript
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');
|
|
});
|
|
});
|