387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
373 lines
12 KiB
TypeScript
373 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 type {
|
|
CanvasLayer,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasPanelDockView } from './ImageCanvasPanelDockView';
|
|
import {
|
|
ImageCanvasSidebarView,
|
|
type ImageCanvasSidebarViewProps,
|
|
} from './ImageCanvasSidebarView';
|
|
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
|
|
|
function createAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
|
|
return {
|
|
id: 'asset-1',
|
|
label: '账号素材A',
|
|
src: '/creation-type-references/puzzle.webp',
|
|
width: 640,
|
|
height: 640,
|
|
folderId: 'project',
|
|
sourceKind: 'uploaded',
|
|
sourceType: 'uploaded',
|
|
persisted: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createFolder(
|
|
overrides: Partial<EditorAssetFolder> = {},
|
|
): EditorAssetFolder {
|
|
return {
|
|
id: 'project',
|
|
label: '项目素材',
|
|
collapsed: false,
|
|
systemDefault: true,
|
|
persisted: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-1',
|
|
resourceId: 'resource-1',
|
|
title: '图层A',
|
|
src: '/creation-type-references/puzzle.webp',
|
|
x: 0,
|
|
y: 0,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 320,
|
|
originalHeight: 240,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createSidebarProps(
|
|
overrides: Partial<ImageCanvasSidebarViewProps> = {},
|
|
): ImageCanvasSidebarViewProps {
|
|
const folder = createFolder();
|
|
const asset = createAsset();
|
|
|
|
return {
|
|
activeSidebarPanel: 'assets',
|
|
assetListRef: createRef<HTMLDivElement>(),
|
|
assetPointerDragRef: { current: null },
|
|
suppressAssetClickRef: { current: false },
|
|
assets: [asset],
|
|
groupedAssets: [{ ...folder, assets: [asset] }],
|
|
assetFolders: [folder],
|
|
layers: [createLayer()],
|
|
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 })),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function SidebarTabsHarness() {
|
|
const chrome = useImageCanvasEditorChrome({ openEditorLoginModal: vi.fn() });
|
|
const asset = createAsset();
|
|
const folder = createFolder();
|
|
const layer = createLayer({ title: '图层B', zIndex: 2 });
|
|
const sidebarProps = createSidebarProps({
|
|
activeSidebarPanel: chrome.activeSidebarPanel,
|
|
assets: [asset],
|
|
groupedAssets: [{ ...folder, assets: [asset] }],
|
|
assetFolders: [folder],
|
|
layers: [layer],
|
|
selectedLayerId: null,
|
|
selectedLayerIds: [],
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<ImageCanvasSidebarView {...sidebarProps} />
|
|
<ImageCanvasPanelDockView
|
|
viewport={{ x: 0, y: 0, scale: 1 }}
|
|
canvasBackgroundColor={chrome.canvasBackgroundColor}
|
|
canvasBackgroundHexValue={chrome.canvasBackgroundHexValue}
|
|
canUndo={false}
|
|
canRedo={false}
|
|
isZoomMenuOpen={false}
|
|
isBackgroundSettingsOpen={false}
|
|
activeSidebarPanel={chrome.activeSidebarPanel}
|
|
isAgentConversationEnabled={false}
|
|
isAgentConversationOpen={false}
|
|
isMinimapOpen={false}
|
|
minimapModel={null}
|
|
onFitLayers={vi.fn()}
|
|
onUndoCanvasChange={vi.fn()}
|
|
onRedoCanvasChange={vi.fn()}
|
|
onUpdateScaleFromCenter={vi.fn()}
|
|
onToggleZoomMenu={vi.fn()}
|
|
onCloseZoomMenu={vi.fn()}
|
|
onToggleBackgroundSettings={vi.fn()}
|
|
onApplyCanvasBackgroundColor={vi.fn()}
|
|
onCanvasBackgroundHexChange={vi.fn()}
|
|
onToggleSidebarPanel={chrome.toggleSidebarPanel}
|
|
onToggleAgentConversation={vi.fn()}
|
|
onToggleMinimap={vi.fn()}
|
|
onMinimapPointerDown={vi.fn()}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe('ImageCanvasSidebarView', () => {
|
|
it('renders the asset library folders and asset actions', () => {
|
|
render(<ImageCanvasSidebarView {...createSidebarProps()} />);
|
|
|
|
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
|
const folder = within(sidebar).getByRole('region', { name: '项目素材' });
|
|
|
|
expect(within(sidebar).getByRole('heading', { name: '素材' })).toBeTruthy();
|
|
expect(
|
|
within(folder).getByRole('button', { name: '添加账号素材A' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(folder).getByRole('button', { name: '上传到项目素材' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(folder).getByRole('button', { name: '下载素材账号素材A' }),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it('renders asset editing inputs with sidebar text-field chrome', () => {
|
|
const folder = createFolder();
|
|
const asset = createAsset();
|
|
|
|
render(
|
|
<ImageCanvasSidebarView
|
|
{...createSidebarProps({
|
|
assets: [asset],
|
|
groupedAssets: [{ ...folder, assets: [asset] }],
|
|
assetFolders: [folder],
|
|
creatingFolder: true,
|
|
newFolderName: '角色上传',
|
|
renamingFolder: { folderId: folder.id, value: '角色参考' },
|
|
renamingAsset: { assetId: asset.id, value: '主视觉素材' },
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const folderNameInput = screen.getByLabelText('素材文件夹名称');
|
|
const folderRenameInput = screen.getByLabelText('重命名文件夹项目素材');
|
|
const assetRenameInput = screen.getByLabelText('重命名素材账号素材A');
|
|
|
|
expect(folderNameInput.className).toContain('platform-text-field');
|
|
expect(folderNameInput.className).toContain(
|
|
'image-canvas-editor__folder-create-input',
|
|
);
|
|
expect(folderRenameInput.className).toContain('platform-text-field');
|
|
expect(folderRenameInput.className).toContain(
|
|
'image-canvas-editor__folder-rename-input',
|
|
);
|
|
expect(assetRenameInput.className).toContain('platform-text-field');
|
|
expect(assetRenameInput.className).toContain(
|
|
'image-canvas-editor__asset-rename-input',
|
|
);
|
|
});
|
|
|
|
it('filters assets from the sidebar search field', () => {
|
|
const folder = createFolder();
|
|
render(
|
|
<ImageCanvasSidebarView
|
|
{...createSidebarProps({
|
|
assets: [
|
|
createAsset({ id: 'asset-a', label: '账号素材A' }),
|
|
createAsset({ id: 'asset-b', label: '火焰按钮' }),
|
|
],
|
|
groupedAssets: [
|
|
{
|
|
...folder,
|
|
assets: [
|
|
createAsset({ id: 'asset-a', label: '账号素材A' }),
|
|
createAsset({ id: 'asset-b', label: '火焰按钮' }),
|
|
],
|
|
},
|
|
],
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
|
fireEvent.change(within(sidebar).getByLabelText('搜索素材'), {
|
|
target: { value: '火焰' },
|
|
});
|
|
|
|
expect(
|
|
within(sidebar).getByRole('button', { name: '添加火焰按钮' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: '添加账号素材A' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('renders the layer panel with grouped and locked layer details', () => {
|
|
render(
|
|
<ImageCanvasSidebarView
|
|
{...createSidebarProps({
|
|
activeSidebarPanel: 'layers',
|
|
layers: [
|
|
createLayer({
|
|
title: '图层A',
|
|
groupId: 'group-1',
|
|
locked: true,
|
|
zIndex: 3,
|
|
}),
|
|
],
|
|
selectedLayerId: 'layer-1',
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
|
|
|
expect(within(sidebar).getByRole('heading', { name: '图层' })).toBeTruthy();
|
|
expect(
|
|
within(sidebar).getByRole('button', { name: '选择图层图层A' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(sidebar).getByText('320 x 240 · 已打组 · 已锁定'),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it('uses the transparent audio cover for audio layer thumbnails', () => {
|
|
render(
|
|
<ImageCanvasSidebarView
|
|
{...createSidebarProps({
|
|
activeSidebarPanel: 'layers',
|
|
layers: [
|
|
createLayer({
|
|
title: '胜利音效',
|
|
src: '/generated-character-drafts/editor-audios/win.mp3',
|
|
mediaType: 'audio',
|
|
}),
|
|
],
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const image = screen.getByAltText('图层缩略图:胜利音效');
|
|
expect(image.getAttribute('src')).toBe(
|
|
'/creation-home/audio-asset-cover.png',
|
|
);
|
|
});
|
|
|
|
it('filters canvas layers from the sidebar search field', () => {
|
|
render(
|
|
<ImageCanvasSidebarView
|
|
{...createSidebarProps({
|
|
activeSidebarPanel: 'layers',
|
|
layers: [
|
|
createLayer({ id: 'layer-a', title: '角色草图', zIndex: 3 }),
|
|
createLayer({ id: 'layer-b', title: '背景山脉', zIndex: 2 }),
|
|
],
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
|
fireEvent.change(within(sidebar).getByLabelText('搜索画布图层'), {
|
|
target: { value: '背景' },
|
|
});
|
|
|
|
expect(
|
|
within(sidebar).getByRole('button', { name: '选择图层背景山脉' }),
|
|
).toBeTruthy();
|
|
expect(
|
|
within(sidebar).queryByRole('button', { name: '选择图层角色草图' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('keeps sidebar tab switching and repeated-click closing behavior', () => {
|
|
render(<SidebarTabsHarness />);
|
|
|
|
expect(
|
|
screen.queryByRole('complementary', { name: '图片资源栏' }),
|
|
).toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开素材' }));
|
|
|
|
expect(screen.getByRole('heading', { name: '素材' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '添加账号素材A' })).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开图层' }));
|
|
|
|
expect(screen.getByRole('heading', { name: '图层' })).toBeTruthy();
|
|
expect(screen.getByRole('button', { name: '选择图层图层B' })).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '打开图层' }));
|
|
|
|
expect(
|
|
screen.queryByRole('complementary', { name: '图片资源栏' }),
|
|
).toBeNull();
|
|
});
|
|
});
|