b8e6e9f3ef
新增 VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR 控制画布 Agent 入口显示。 默认 .env 关闭、本地 .env.local 开启,并补充示例和类型声明。 禁用时隐藏 dock 按钮和收起态 Agent 面板,开启时保留右侧任务列表互斥逻辑。 修正右侧 Agent 和任务列表切换不影响左侧资源栏。 补充画布 Agent 开关、面板独立性和 dock 隐藏测试。
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 { ImageCanvasPanelDockView } from './ImageCanvasPanelDockView';
|
|
import {
|
|
ImageCanvasSidebarView,
|
|
type ImageCanvasSidebarViewProps,
|
|
} from './ImageCanvasSidebarView';
|
|
import type {
|
|
CanvasLayer,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
} from './ImageCanvasEditorTypes';
|
|
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();
|
|
});
|
|
});
|