画布图片 hover 尺寸标签改为复用 PlatformPillBadge,统一覆盖层 badge 基础结构。 删除尺寸标签局部圆角、字号和排版样式,仅保留画布内定位与深色覆盖。 补充编辑器测试覆盖共享胶囊标签 class,并更新 TRACKING。
1133 lines
42 KiB
TypeScript
1133 lines
42 KiB
TypeScript
/* @vitest-environment jsdom */
|
||
|
||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
||
import { ApiClientError } from '../../services/apiClient';
|
||
import { ImageCanvasEditorView } from './ImageCanvasEditorView';
|
||
|
||
const generateEditorImageMock = vi.hoisted(() => vi.fn());
|
||
const editEditorImageMock = vi.hoisted(() => vi.fn());
|
||
const createEditorAssetMock = vi.hoisted(() => vi.fn());
|
||
const createEditorProjectResourceMock = vi.hoisted(() => vi.fn());
|
||
const createEditorAssetFolderMock = vi.hoisted(() => vi.fn());
|
||
const updateEditorAssetFolderMock = vi.hoisted(() => vi.fn());
|
||
const deleteEditorAssetFolderMock = vi.hoisted(() => vi.fn());
|
||
const deleteEditorAssetMock = vi.hoisted(() => vi.fn());
|
||
const loadEditorAssetLibraryMock = vi.hoisted(() => vi.fn());
|
||
const loadEditorProjectMock = vi.hoisted(() => vi.fn());
|
||
const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
|
||
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
|
||
|
||
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||
const actual = await vi.importActual<
|
||
typeof import('../../services/image-editor/editorProjectClient')
|
||
>('../../services/image-editor/editorProjectClient');
|
||
return {
|
||
...actual,
|
||
editEditorImage: editEditorImageMock,
|
||
createEditorAsset: createEditorAssetMock,
|
||
createEditorAssetFolder: createEditorAssetFolderMock,
|
||
createEditorProjectResource: createEditorProjectResourceMock,
|
||
deleteEditorAsset: deleteEditorAssetMock,
|
||
deleteEditorAssetFolder: deleteEditorAssetFolderMock,
|
||
generateEditorImage: generateEditorImageMock,
|
||
loadEditorAssetLibrary: loadEditorAssetLibraryMock,
|
||
loadEditorProject: loadEditorProjectMock,
|
||
loadOrCreateRecentEditorProject: loadOrCreateRecentEditorProjectMock,
|
||
saveEditorProjectLayout: saveEditorProjectLayoutMock,
|
||
updateEditorAssetFolder: updateEditorAssetFolderMock,
|
||
};
|
||
});
|
||
|
||
function dispatchPointerEvent(
|
||
target: Element,
|
||
type: string,
|
||
init: MouseEventInit & { pointerId: number },
|
||
) {
|
||
const event = new MouseEvent(type, {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
...init,
|
||
});
|
||
Object.defineProperty(event, 'pointerId', { value: init.pointerId });
|
||
fireEvent(target, event);
|
||
}
|
||
|
||
describe('ImageCanvasEditorView', () => {
|
||
beforeEach(() => {
|
||
loadOrCreateRecentEditorProjectMock.mockResolvedValue({
|
||
projectId: 'editor-project-default',
|
||
title: '默认项目',
|
||
viewport: { x: 0, y: 0, scale: 1 },
|
||
layers: [],
|
||
resources: [],
|
||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||
});
|
||
loadEditorAssetLibraryMock.mockResolvedValue({
|
||
folders: [
|
||
{
|
||
folderId: 'project',
|
||
label: '项目素材',
|
||
sortOrder: 0,
|
||
collapsed: false,
|
||
systemDefault: true,
|
||
},
|
||
],
|
||
assets: [],
|
||
});
|
||
createEditorAssetMock.mockImplementation(async (input) => ({
|
||
assetId: `persisted-${input.label}`,
|
||
folderId: input.folderId,
|
||
label: input.label,
|
||
imageSrc: input.imageSrc,
|
||
width: input.width,
|
||
height: input.height,
|
||
sourceType: input.sourceType,
|
||
}));
|
||
createEditorAssetFolderMock.mockResolvedValue({
|
||
folderId: 'folder-role-persisted',
|
||
label: '角色上传',
|
||
collapsed: false,
|
||
systemDefault: false,
|
||
});
|
||
updateEditorAssetFolderMock.mockImplementation(async (folderId, input) => ({
|
||
folderId,
|
||
label: input.label ?? '角色上传',
|
||
collapsed: input.collapsed ?? false,
|
||
systemDefault: false,
|
||
}));
|
||
deleteEditorAssetFolderMock.mockResolvedValue({
|
||
folders: [
|
||
{
|
||
folderId: 'project',
|
||
label: '项目素材',
|
||
sortOrder: 0,
|
||
collapsed: false,
|
||
systemDefault: true,
|
||
},
|
||
],
|
||
assets: [],
|
||
});
|
||
deleteEditorAssetMock.mockResolvedValue({});
|
||
createEditorProjectResourceMock.mockImplementation(async (projectId, input) => ({
|
||
resourceId: `resource-${projectId}-${input.width}`,
|
||
projectId,
|
||
imageSrc: input.imageSrc,
|
||
width: input.width,
|
||
height: input.height,
|
||
sourceType: input.sourceType,
|
||
}));
|
||
saveEditorProjectLayoutMock.mockResolvedValue({});
|
||
});
|
||
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
vi.restoreAllMocks();
|
||
generateEditorImageMock.mockReset();
|
||
editEditorImageMock.mockReset();
|
||
createEditorAssetMock.mockReset();
|
||
createEditorProjectResourceMock.mockReset();
|
||
createEditorAssetFolderMock.mockReset();
|
||
updateEditorAssetFolderMock.mockReset();
|
||
deleteEditorAssetFolderMock.mockReset();
|
||
deleteEditorAssetMock.mockReset();
|
||
loadEditorAssetLibraryMock.mockReset();
|
||
loadEditorProjectMock.mockReset();
|
||
loadOrCreateRecentEditorProjectMock.mockReset();
|
||
saveEditorProjectLayoutMock.mockReset();
|
||
window.history.replaceState(null, '', '/editor/canvas');
|
||
});
|
||
|
||
it('loads the project from projectid query before falling back to recent project', async () => {
|
||
loadEditorProjectMock.mockResolvedValueOnce({
|
||
projectId: 'editor-project-query',
|
||
title: '查询项目',
|
||
viewport: { x: 12, y: 16, scale: 0.8 },
|
||
layers: [],
|
||
resources: [],
|
||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||
});
|
||
window.history.replaceState(
|
||
null,
|
||
'',
|
||
'/editor/canvas?projectid=editor-project-query',
|
||
);
|
||
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
await waitFor(() => {
|
||
expect(loadEditorProjectMock).toHaveBeenCalledWith('editor-project-query');
|
||
});
|
||
expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('toggles the shared sidebar from canvas panel buttons', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||
const panelToolbar = screen.getByRole('toolbar', { name: '画布面板入口' });
|
||
const assetsButton = within(panelToolbar).getByRole('button', { name: '打开素材' });
|
||
const layersButton = within(panelToolbar).getByRole('button', { name: '打开图层' });
|
||
|
||
expect(within(sidebar).getByText('素材')).toBeTruthy();
|
||
expect(within(sidebar).getByRole('button', { name: '添加拼图素材' })).toBeTruthy();
|
||
expect(assetsButton.getAttribute('aria-pressed')).toBe('true');
|
||
expect(screen.queryByRole('button', { name: '打开已生成文件' })).toBeNull();
|
||
expect(screen.queryByRole('button', { name: '收起素材栏' })).toBeNull();
|
||
expect(screen.queryByRole('button', { name: '展开素材栏' })).toBeNull();
|
||
|
||
fireEvent.click(layersButton);
|
||
|
||
const layerSidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||
expect(within(layerSidebar).getByText('图层')).toBeTruthy();
|
||
expect(
|
||
within(layerSidebar).getByRole('button', { name: '选择图层拼图素材' }),
|
||
).toBeTruthy();
|
||
expect(layersButton.getAttribute('aria-pressed')).toBe('true');
|
||
expect(screen.queryByRole('button', { name: '添加拼图素材' })).toBeNull();
|
||
|
||
fireEvent.click(layersButton);
|
||
|
||
expect(screen.queryByRole('complementary', { name: '图片资源栏' })).toBeNull();
|
||
expect(layersButton.getAttribute('aria-pressed')).toBe('false');
|
||
});
|
||
|
||
it('groups assets by folder and renames sidebar materials', async () => {
|
||
const user = userEvent.setup();
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||
expect(within(sidebar).getByRole('region', { name: '项目素材' })).toBeTruthy();
|
||
expect(within(sidebar).getByRole('region', { name: '参考素材' })).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: '重命名素材拼图素材' }));
|
||
const renameInput = screen.getByLabelText('重命名素材拼图素材');
|
||
expect(renameInput.className).toContain('platform-text-field');
|
||
expect(renameInput.className).toContain(
|
||
'image-canvas-editor__asset-rename-input',
|
||
);
|
||
await user.clear(renameInput);
|
||
await user.type(renameInput, '主视觉素材');
|
||
await user.click(screen.getByRole('button', { name: '保存素材拼图素材名称' }));
|
||
|
||
expect(screen.queryByRole('button', { name: '添加拼图素材' })).toBeNull();
|
||
await user.click(screen.getByRole('button', { name: '添加主视觉素材' }));
|
||
|
||
expect(screen.getByAltText('画布图片:主视觉素材')).toBeTruthy();
|
||
});
|
||
|
||
it('collapses folders, creates upload folders, and deletes uploaded materials', async () => {
|
||
const user = userEvent.setup();
|
||
const createObjectUrlSpy = vi.fn(() => 'blob:folder-uploaded-image');
|
||
Object.defineProperty(URL, 'createObjectURL', {
|
||
configurable: true,
|
||
value: createObjectUrlSpy,
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
await user.click(screen.getByRole('button', { name: '折叠项目素材' }));
|
||
expect(screen.queryByRole('button', { name: '添加拼图素材' })).toBeNull();
|
||
await user.click(screen.getByRole('button', { name: '展开项目素材' }));
|
||
expect(screen.getByRole('button', { name: '添加拼图素材' })).toBeTruthy();
|
||
|
||
await user.click(screen.getByRole('button', { name: '新建素材文件夹' }));
|
||
const folderNameInput = screen.getByLabelText('素材文件夹名称');
|
||
expect(folderNameInput.className).toContain('platform-text-field');
|
||
expect(folderNameInput.className).toContain(
|
||
'image-canvas-editor__folder-create-input',
|
||
);
|
||
await user.type(folderNameInput, '角色上传');
|
||
await user.click(screen.getByRole('button', { name: '保存素材文件夹' }));
|
||
|
||
const uploadInput = screen.getByLabelText('上传图片文件');
|
||
await user.click(screen.getByRole('button', { name: '上传到角色上传' }));
|
||
await userEvent.upload(
|
||
uploadInput,
|
||
new File(['image'], '角色草图.png', { type: 'image/png' }),
|
||
);
|
||
|
||
await user.click(screen.getByRole('button', { name: '打开素材' }));
|
||
const customFolder = screen.getByRole('region', { name: '角色上传' });
|
||
expect(within(customFolder).getByRole('button', { name: '添加角色草图.png' })).toBeTruthy();
|
||
expect(within(customFolder).getByRole('button', { name: '删除素材角色草图.png' })).toBeTruthy();
|
||
|
||
await user.click(within(customFolder).getByRole('button', { name: '删除素材角色草图.png' }));
|
||
|
||
expect(screen.queryByRole('button', { name: '添加角色草图.png' })).toBeNull();
|
||
expect(screen.getByAltText('画布图片:角色草图.png')).toBeTruthy();
|
||
});
|
||
|
||
it('renames and deletes asset folders through the persisted asset library API', async () => {
|
||
const user = userEvent.setup();
|
||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||
folders: [
|
||
{
|
||
folderId: 'project',
|
||
label: '项目素材',
|
||
sortOrder: 0,
|
||
collapsed: false,
|
||
systemDefault: true,
|
||
},
|
||
{
|
||
folderId: 'folder-role',
|
||
label: '角色',
|
||
sortOrder: 100,
|
||
collapsed: false,
|
||
systemDefault: false,
|
||
},
|
||
],
|
||
assets: [],
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
await screen.findByRole('region', { name: '角色' });
|
||
await user.click(screen.getByRole('button', { name: '重命名文件夹角色' }));
|
||
const folderRenameInput = screen.getByLabelText('重命名文件夹角色');
|
||
expect(folderRenameInput.className).toContain('platform-text-field');
|
||
expect(folderRenameInput.className).toContain(
|
||
'image-canvas-editor__folder-rename-input',
|
||
);
|
||
await user.clear(folderRenameInput);
|
||
await user.type(folderRenameInput, '角色参考');
|
||
await user.click(screen.getByRole('button', { name: '保存文件夹角色名称' }));
|
||
|
||
expect(updateEditorAssetFolderMock).toHaveBeenCalledWith('folder-role', {
|
||
label: '角色参考',
|
||
});
|
||
|
||
await user.click(screen.getByRole('button', { name: '删除文件夹角色参考' }));
|
||
|
||
expect(deleteEditorAssetFolderMock).toHaveBeenCalledWith('folder-role');
|
||
});
|
||
|
||
it('uploads multiple files and persists them as account-level assets', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
|
||
fireEvent.click(within(bottomToolbar).getByRole('button', { name: '上传工具' }));
|
||
await userEvent.upload(screen.getByLabelText('上传图片文件'), [
|
||
new File(['image-a'], '第一张.png', { type: 'image/png' }),
|
||
new File(['image-b'], '第二张.png', { type: 'image/png' }),
|
||
]);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByAltText('画布图片:第一张.png')).toBeTruthy();
|
||
expect(screen.getByAltText('画布图片:第二张.png')).toBeTruthy();
|
||
});
|
||
expect(createEditorAssetMock).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it('supports asset selection mode and batch delete with shared toolbar', async () => {
|
||
const user = userEvent.setup();
|
||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||
folders: [
|
||
{
|
||
folderId: 'project',
|
||
label: '项目素材',
|
||
sortOrder: 0,
|
||
collapsed: false,
|
||
systemDefault: true,
|
||
},
|
||
],
|
||
assets: [
|
||
{
|
||
assetId: 'asset-a',
|
||
folderId: 'project',
|
||
label: '账号素材A',
|
||
imageSrc: 'data:image/png;base64,YQ==',
|
||
width: 320,
|
||
height: 240,
|
||
sourceType: 'uploaded',
|
||
},
|
||
{
|
||
assetId: 'asset-b',
|
||
folderId: 'project',
|
||
label: '账号素材B',
|
||
imageSrc: 'data:image/png;base64,Yg==',
|
||
width: 320,
|
||
height: 240,
|
||
sourceType: 'uploaded',
|
||
},
|
||
],
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
await screen.findByRole('button', { name: '添加账号素材A' });
|
||
await user.click(screen.getByRole('button', { name: '素材选择模式' }));
|
||
await user.click(screen.getByRole('button', { name: '选择素材账号素材A' }));
|
||
|
||
const batchToolbar = screen.getByRole('toolbar', { name: '素材批量操作' });
|
||
expect(within(batchToolbar).getByText(/已选 1/u)).toBeTruthy();
|
||
await user.click(within(batchToolbar).getByRole('button', { name: '删除' }));
|
||
|
||
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-a');
|
||
expect(screen.queryByRole('button', { name: '选择素材账号素材A' })).toBeNull();
|
||
});
|
||
|
||
it('selects multiple assets with a marquee in asset selection mode', async () => {
|
||
const user = userEvent.setup();
|
||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||
folders: [
|
||
{
|
||
folderId: 'project',
|
||
label: '项目素材',
|
||
sortOrder: 0,
|
||
collapsed: false,
|
||
systemDefault: true,
|
||
},
|
||
],
|
||
assets: [
|
||
{
|
||
assetId: 'asset-a',
|
||
folderId: 'project',
|
||
label: '账号素材A',
|
||
imageSrc: 'data:image/png;base64,YQ==',
|
||
width: 320,
|
||
height: 240,
|
||
sourceType: 'uploaded',
|
||
},
|
||
{
|
||
assetId: 'asset-b',
|
||
folderId: 'project',
|
||
label: '账号素材B',
|
||
imageSrc: 'data:image/png;base64,Yg==',
|
||
width: 320,
|
||
height: 240,
|
||
sourceType: 'uploaded',
|
||
},
|
||
],
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const firstAssetButton = await screen.findByRole('button', {
|
||
name: '添加账号素材A',
|
||
});
|
||
const secondAssetButton = screen.getByRole('button', { name: '添加账号素材B' });
|
||
const assetList = firstAssetButton.closest(
|
||
'.image-canvas-editor__asset-list',
|
||
) as HTMLElement;
|
||
vi.spyOn(assetList, 'getBoundingClientRect').mockReturnValue({
|
||
x: 0,
|
||
y: 0,
|
||
left: 0,
|
||
top: 0,
|
||
right: 320,
|
||
bottom: 600,
|
||
width: 320,
|
||
height: 600,
|
||
toJSON: () => ({}),
|
||
});
|
||
vi.spyOn(
|
||
firstAssetButton.closest('[data-asset-id]') as HTMLElement,
|
||
'getBoundingClientRect',
|
||
).mockReturnValue({
|
||
x: 16,
|
||
y: 120,
|
||
left: 16,
|
||
top: 120,
|
||
right: 280,
|
||
bottom: 200,
|
||
width: 264,
|
||
height: 80,
|
||
toJSON: () => ({}),
|
||
});
|
||
vi.spyOn(
|
||
secondAssetButton.closest('[data-asset-id]') as HTMLElement,
|
||
'getBoundingClientRect',
|
||
).mockReturnValue({
|
||
x: 16,
|
||
y: 240,
|
||
left: 16,
|
||
top: 240,
|
||
right: 280,
|
||
bottom: 320,
|
||
width: 264,
|
||
height: 80,
|
||
toJSON: () => ({}),
|
||
});
|
||
|
||
await user.click(screen.getByRole('button', { name: '素材选择模式' }));
|
||
dispatchPointerEvent(assetList, 'pointerdown', {
|
||
button: 0,
|
||
pointerId: 88,
|
||
clientX: 8,
|
||
clientY: 100,
|
||
});
|
||
dispatchPointerEvent(assetList, 'pointermove', {
|
||
button: 0,
|
||
pointerId: 88,
|
||
clientX: 300,
|
||
clientY: 330,
|
||
});
|
||
dispatchPointerEvent(assetList, 'pointerup', {
|
||
button: 0,
|
||
pointerId: 88,
|
||
clientX: 300,
|
||
clientY: 330,
|
||
});
|
||
|
||
const batchToolbar = screen.getByRole('toolbar', { name: '素材批量操作' });
|
||
expect(within(batchToolbar).getByText(/已选 2/u)).toBeTruthy();
|
||
});
|
||
|
||
it('shows image size on hover and placeholder toolbar after selecting a layer', () => {
|
||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const canvasImage = screen.getByAltText('画布图片:拼图素材');
|
||
fireEvent.mouseEnter(canvasImage.closest('button')!);
|
||
|
||
const sizeBadge = screen.getByText('420 x 420 px');
|
||
expect(sizeBadge.className).toContain('rounded-full');
|
||
expect(sizeBadge.className).toContain('image-canvas-editor__size-badge');
|
||
|
||
fireEvent.pointerDown(canvasImage.closest('button')!, {
|
||
button: 0,
|
||
pointerId: 1,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
|
||
const cropButton = screen.getByRole('button', { name: '裁剪占位' });
|
||
fireEvent.pointerDown(cropButton, {
|
||
button: 0,
|
||
pointerId: 2,
|
||
clientX: 120,
|
||
clientY: 96,
|
||
});
|
||
fireEvent.click(cropButton);
|
||
|
||
expect(alertSpy).toHaveBeenCalledWith('裁剪功能建设中');
|
||
expect(screen.getByRole('toolbar', { name: '图片工具栏' })).toBeTruthy();
|
||
});
|
||
|
||
it('treats puzzle material as a normal asset without generated metadata tools', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.pointerDown(screen.getByAltText('画布图片:拼图素材').closest('button')!, {
|
||
button: 0,
|
||
pointerId: 61,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
|
||
expect(screen.queryByRole('button', { name: '查看拼图素材元数据' })).toBeNull();
|
||
expect(screen.queryByRole('button', { name: '修改图片' })).toBeNull();
|
||
});
|
||
|
||
it('deletes the selected layer from the floating toolbar', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
expect(screen.getByAltText('画布图片:拼图素材')).toBeTruthy();
|
||
|
||
fireEvent.pointerDown(screen.getByAltText('画布图片:拼图素材').closest('button')!, {
|
||
button: 0,
|
||
pointerId: 51,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '删除图片' }));
|
||
|
||
expect(screen.queryByAltText('画布图片:拼图素材')).toBeNull();
|
||
expect(screen.getByAltText('画布图片:大鱼素材')).toBeTruthy();
|
||
});
|
||
|
||
it('uploads an image file as a new canvas layer', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
await waitFor(() => {
|
||
expect(loadOrCreateRecentEditorProjectMock).toHaveBeenCalled();
|
||
});
|
||
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
|
||
expect(within(bottomToolbar).queryByRole('button', { name: '局部修改工具' })).toBeNull();
|
||
|
||
fireEvent.click(within(bottomToolbar).getByRole('button', { name: '上传工具' }));
|
||
await userEvent.upload(
|
||
screen.getByLabelText('上传图片文件'),
|
||
new File(['image'], '测试上传.png', { type: 'image/png' }),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByAltText('画布图片:测试上传.png')).toBeTruthy();
|
||
});
|
||
expect(createEditorAssetMock).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
label: '测试上传.png',
|
||
imageSrc: expect.stringMatching(/^data:image\/png;base64,/u),
|
||
}),
|
||
);
|
||
expect(screen.getByRole('button', { name: '选择图层测试上传.png' })).toBeTruthy();
|
||
});
|
||
|
||
it('blocks the browser context menu inside the editor workspace', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const editor = screen.getByRole('region', { name: '图片画布编辑器' });
|
||
const contextMenuEvent = new MouseEvent('contextmenu', {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
});
|
||
const wasNotCanceled = editor.dispatchEvent(contextMenuEvent);
|
||
|
||
expect(wasNotCanceled).toBe(false);
|
||
expect(contextMenuEvent.defaultPrevented).toBe(true);
|
||
});
|
||
|
||
it('switches the shared sidebar between assets and layers', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||
expect(within(sidebar).getByText('素材')).toBeTruthy();
|
||
expect(within(sidebar).queryByText('已生成文件')).toBeNull();
|
||
expect(within(sidebar).queryByText('图层')).toBeNull();
|
||
expect(screen.queryByRole('toolbar', { name: '画布主工具栏' })).toBeNull();
|
||
expect(screen.queryByRole('complementary', { name: '图层面板' })).toBeNull();
|
||
expect(screen.queryByRole('dialog', { name: '已生成文件' })).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '打开图层' }));
|
||
const layersPanel = screen.getByRole('complementary', { name: '图片资源栏' });
|
||
expect(
|
||
within(layersPanel).getByRole('button', { name: '选择图层拼图素材' }),
|
||
).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '选择图层大鱼素材' }));
|
||
|
||
expect(screen.getByRole('toolbar', { name: '图片工具栏' })).toBeTruthy();
|
||
expect(screen.queryByRole('button', { name: '查看大鱼素材元数据' })).toBeNull();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '打开素材' }));
|
||
|
||
expect(screen.getByRole('button', { name: '添加拼图素材' })).toBeTruthy();
|
||
});
|
||
|
||
it('adds assets from the sidebar and supports zoom buttons', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
expect(
|
||
screen.getByRole('button', { name: '当前缩放比例 82%' }).className,
|
||
).toContain('platform-inline-option-button');
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '当前缩放比例 82%' }));
|
||
fireEvent.click(screen.getByRole('menuitem', { name: '放大' }));
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 95%' })).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '添加声浪素材' }));
|
||
|
||
expect(screen.getByAltText('画布图片:声浪素材')).toBeTruthy();
|
||
expect(screen.getByRole('complementary', { name: '图片资源栏' })).toBeTruthy();
|
||
});
|
||
|
||
it('offers Lovart-style zoom menu commands', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '当前缩放比例 82%' }));
|
||
|
||
expect(screen.getByRole('menu', { name: '缩放菜单' })).toBeTruthy();
|
||
expect(screen.getByRole('menuitem', { name: '显示画布所有元素' })).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至100%' }));
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 100%' })).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '当前缩放比例 100%' }));
|
||
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' }));
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 50%' })).toBeTruthy();
|
||
});
|
||
|
||
it('shows the Lovart-style minimap and canvas background controls', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const viewport = screen.getByLabelText('画布工作区');
|
||
const panelToolbar = screen.getByRole('toolbar', { name: '画布面板入口' });
|
||
|
||
expect(screen.getByRole('button', { name: '画布小地图' })).toBeTruthy();
|
||
expect(
|
||
within(panelToolbar).getByRole('button', { name: '画布背景色' }).className,
|
||
).toContain('platform-icon-button');
|
||
expect(within(panelToolbar).getByRole('button', { name: '切换小地图' })).toBeTruthy();
|
||
|
||
fireEvent.click(within(panelToolbar).getByRole('button', { name: '画布背景色' }));
|
||
expect(screen.getByRole('menu', { name: '画布背景色菜单' })).toBeTruthy();
|
||
fireEvent.click(screen.getByRole('menuitem', { name: '切换画布背景色为暖灰' }));
|
||
|
||
expect((viewport as HTMLElement).style.backgroundColor).toBe('rgb(243, 240, 234)');
|
||
|
||
fireEvent.click(within(panelToolbar).getByRole('button', { name: '切换小地图' }));
|
||
expect(screen.queryByRole('button', { name: '画布小地图' })).toBeNull();
|
||
});
|
||
|
||
it('uses normal wheel for vertical canvas scroll and ctrl wheel for zoom', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const viewport = screen.getByLabelText('画布工作区');
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 82%' })).toBeTruthy();
|
||
|
||
fireEvent.wheel(viewport, { deltaY: 120, clientX: 400, clientY: 280 });
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 82%' })).toBeTruthy();
|
||
|
||
fireEvent.wheel(viewport, {
|
||
deltaY: -120,
|
||
ctrlKey: true,
|
||
clientX: 400,
|
||
clientY: 280,
|
||
});
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 90%' })).toBeTruthy();
|
||
});
|
||
|
||
it('drags the minimap to move the canvas viewport', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const minimap = screen.getByRole('button', { name: '画布小地图' });
|
||
vi.spyOn(minimap, 'getBoundingClientRect').mockReturnValue({
|
||
x: 0,
|
||
y: 0,
|
||
left: 0,
|
||
top: 0,
|
||
right: 132,
|
||
bottom: 84,
|
||
width: 132,
|
||
height: 84,
|
||
toJSON: () => ({}),
|
||
});
|
||
dispatchPointerEvent(minimap, 'pointerdown', {
|
||
button: 0,
|
||
pointerId: 71,
|
||
clientX: 120,
|
||
clientY: 72,
|
||
});
|
||
|
||
const firstLayer = screen.getByAltText('画布图片:拼图素材').closest('button')!;
|
||
expect(Number.parseFloat((firstLayer as HTMLElement).style.left)).toBe(470);
|
||
expect(screen.getByRole('button', { name: '画布小地图' })).toBeTruthy();
|
||
});
|
||
|
||
it('persists layer groups in the canvas layer snapshot', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '打开图层' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '图层打组' }));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText(/已打组/u)).toBeTruthy();
|
||
});
|
||
await waitFor(() => {
|
||
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
||
'editor-project-default',
|
||
expect.objectContaining({
|
||
layers: expect.arrayContaining([
|
||
expect.objectContaining({
|
||
title: '拼图素材',
|
||
groupId: expect.stringMatching(/^layer-group-/u),
|
||
}),
|
||
]),
|
||
}),
|
||
);
|
||
});
|
||
});
|
||
|
||
it('opens a canvas generation frame and composer before creating a generated layer', async () => {
|
||
generateEditorImageMock.mockResolvedValueOnce({
|
||
imageSrc: 'data:image/png;base64,ZmFrZS1pbWFnZQ==',
|
||
width: 1024,
|
||
height: 1024,
|
||
sourceType: 'generated',
|
||
prompt: '一张明亮的拼图主视觉',
|
||
actualPrompt: '一张明亮的拼图主视觉',
|
||
model: 'gpt-image-2',
|
||
provider: 'VectorEngine',
|
||
taskId: 'editor-real-task-1',
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
fireEvent.click(within(bottomToolbar).getByRole('button', { name: '生成工具' }));
|
||
|
||
const generateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||
const initialComposerTop = Number.parseFloat(
|
||
(generateDialog as HTMLElement).style.top,
|
||
);
|
||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||
expect(within(generateDialog).getByText('参考图')).toBeTruthy();
|
||
expect(
|
||
within(generateDialog).getByRole('button', { name: '添加参考图' }).className,
|
||
).toContain('bg-white/94');
|
||
expect(
|
||
within(generateDialog).getByRole('button', { name: '添加参考图' }).className,
|
||
).toContain('image-canvas-editor__generation-ref');
|
||
const generatePrompt = screen.getByLabelText('生成提示词');
|
||
expect(generatePrompt.className).toContain('platform-text-field');
|
||
expect(generatePrompt.className).toContain(
|
||
'image-canvas-editor__generation-prompt',
|
||
);
|
||
expect(
|
||
within(generateDialog).getByRole('button', {
|
||
name: '生成比例 1:1 2k 1张',
|
||
}).className,
|
||
).toContain('platform-inline-option-button');
|
||
expect(
|
||
within(generateDialog).getByRole('button', {
|
||
name: '生成模型 GPT Image',
|
||
}).className,
|
||
).toContain('platform-inline-option-button');
|
||
expect(within(generateDialog).getByRole('button', { name: '生成' }).className).toContain(
|
||
'platform-button',
|
||
);
|
||
expect(within(generateDialog).getByRole('button', { name: '生成' }).className).toContain(
|
||
'image-canvas-editor__generation-submit',
|
||
);
|
||
expect(screen.queryByRole('toolbar', { name: 'AI画布工具栏' })).toBeNull();
|
||
|
||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||
target: { value: '一张明亮的拼图主视觉' },
|
||
});
|
||
fireEvent.click(within(generateDialog).getByRole('button', { name: '生成' }));
|
||
|
||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||
prompt: '一张明亮的拼图主视觉',
|
||
});
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||
});
|
||
const generatedLayer = screen.getByAltText(/画布图片:生成图片/).closest('button')!;
|
||
const anchoredGenerateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||
expect(anchoredGenerateDialog).toBeTruthy();
|
||
expect(
|
||
Number.parseFloat((anchoredGenerateDialog as HTMLElement).style.top),
|
||
).toBeGreaterThan(Number.parseFloat((generatedLayer as HTMLElement).style.top));
|
||
expect(
|
||
Number.parseFloat((generatedLayer as HTMLElement).style.top),
|
||
).toBeLessThan(initialComposerTop);
|
||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||
const metadataButtons = screen.getAllByRole('button', {
|
||
name: /查看生成图片 .*元数据/,
|
||
});
|
||
expect(metadataButtons[0]).toBeTruthy();
|
||
});
|
||
|
||
it('drags the generation placeholder and places the generated layer there', async () => {
|
||
generateEditorImageMock.mockResolvedValueOnce({
|
||
imageSrc: 'data:image/png;base64,ZHJhZ2dlZC1mcmFtZQ==',
|
||
width: 1024,
|
||
height: 1024,
|
||
sourceType: 'generated',
|
||
prompt: '拖拽后的生成图',
|
||
actualPrompt: '拖拽后的生成图',
|
||
model: 'gpt-image-2',
|
||
provider: 'VectorEngine',
|
||
taskId: 'editor-drag-frame-1',
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
await waitFor(() => {
|
||
expect(loadOrCreateRecentEditorProjectMock).toHaveBeenCalled();
|
||
});
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
const initialComposerTop = Number.parseFloat(
|
||
(screen.getByRole('dialog', { name: '生成图片' }) as HTMLElement).style.top,
|
||
);
|
||
const frame = screen.getByLabelText('图像生成占位图');
|
||
dispatchPointerEvent(frame, 'pointerdown', {
|
||
button: 0,
|
||
pointerId: 61,
|
||
clientX: 500,
|
||
clientY: 260,
|
||
});
|
||
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
|
||
pointerId: 61,
|
||
clientX: 582,
|
||
clientY: 342,
|
||
});
|
||
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointerup', {
|
||
pointerId: 61,
|
||
clientX: 582,
|
||
clientY: 342,
|
||
});
|
||
const draggedComposerTop = Number.parseFloat(
|
||
(screen.getByRole('dialog', { name: '生成图片' }) as HTMLElement).style.top,
|
||
);
|
||
expect(draggedComposerTop).toBeGreaterThan(initialComposerTop);
|
||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||
target: { value: '拖拽后的生成图' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||
});
|
||
|
||
const generatedLayer = screen.getByAltText(/画布图片:生成图片/).closest('button')!;
|
||
const anchoredGenerateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||
expect(anchoredGenerateDialog).toBeTruthy();
|
||
expect(
|
||
Number.parseFloat((anchoredGenerateDialog as HTMLElement).style.top),
|
||
).toBeGreaterThan(Number.parseFloat((generatedLayer as HTMLElement).style.top));
|
||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||
expect(Number.parseFloat((generatedLayer as HTMLElement).style.left)).toBeGreaterThan(300);
|
||
expect(Number.parseFloat((generatedLayer as HTMLElement).style.top)).toBeGreaterThan(180);
|
||
});
|
||
|
||
it('keeps the generation composer when selecting another image', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||
|
||
fireEvent.pointerDown(screen.getByAltText('画布图片:拼图素材').closest('button')!, {
|
||
button: 0,
|
||
pointerId: 62,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
|
||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||
});
|
||
|
||
it('keeps the generation composer when clicking the canvas outside generation controls', () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||
|
||
fireEvent.pointerDown(screen.getByLabelText('画布工作区'), {
|
||
button: 0,
|
||
pointerId: 63,
|
||
clientX: 260,
|
||
clientY: 180,
|
||
});
|
||
|
||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||
});
|
||
|
||
it('shows generation errors instead of falling back to mock images', async () => {
|
||
generateEditorImageMock.mockRejectedValueOnce(new Error('VectorEngine 未配置'));
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||
target: { value: '一张真实生成失败的图' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||
|
||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('alert').textContent).toContain('VectorEngine 未配置');
|
||
});
|
||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||
expect(screen.queryByAltText(/画布图片:生成图片/)).toBeNull();
|
||
});
|
||
|
||
it('asks the user to log in when real generation is unauthorized', async () => {
|
||
generateEditorImageMock.mockRejectedValueOnce(
|
||
new ApiClientError({
|
||
message: '未授权访问(requestId: web-login-required)',
|
||
status: 401,
|
||
code: 'UNAUTHORIZED',
|
||
}),
|
||
);
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||
target: { value: '一张需要登录生成的图' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('alert').textContent).toBe(
|
||
'请先登录后再生成图片',
|
||
);
|
||
});
|
||
expect(screen.queryByText(/requestId/u)).toBeNull();
|
||
});
|
||
|
||
it('switches tools and restores the previous tool after holding Space', async () => {
|
||
const user = userEvent.setup();
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
const selectTool = within(bottomToolbar).getByRole('button', { name: '选择工具' });
|
||
const textTool = within(bottomToolbar).getByRole('button', { name: '文字工具' });
|
||
const handTool = within(bottomToolbar).getByRole('button', { name: '抓手工具' });
|
||
|
||
expect(selectTool.getAttribute('aria-pressed')).toBe('true');
|
||
|
||
await user.click(textTool);
|
||
expect(textTool.getAttribute('aria-pressed')).toBe('true');
|
||
|
||
fireEvent.keyDown(window, { code: 'Space', key: ' ' });
|
||
expect(handTool.getAttribute('aria-pressed')).toBe('true');
|
||
|
||
fireEvent.keyUp(window, { code: 'Space', key: ' ' });
|
||
expect(textTool.getAttribute('aria-pressed')).toBe('true');
|
||
});
|
||
|
||
it('switches away from hand tool from the bottom toolbar', async () => {
|
||
const user = userEvent.setup();
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
const handTool = within(bottomToolbar).getByRole('button', { name: '抓手工具' });
|
||
const textTool = within(bottomToolbar).getByRole('button', { name: '文字工具' });
|
||
|
||
await user.click(handTool);
|
||
expect(handTool.getAttribute('aria-pressed')).toBe('true');
|
||
|
||
await user.click(textTool);
|
||
expect(textTool.getAttribute('aria-pressed')).toBe('true');
|
||
expect(handTool.getAttribute('aria-pressed')).toBe('false');
|
||
});
|
||
|
||
it('pans with the middle mouse button without leaving select mode', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const viewport = screen.getByLabelText('画布工作区');
|
||
const middlePointerDown = new MouseEvent('pointerdown', {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
button: 1,
|
||
buttons: 4,
|
||
clientX: 260,
|
||
clientY: 220,
|
||
});
|
||
Object.defineProperty(middlePointerDown, 'pointerId', { value: 11 });
|
||
fireEvent(viewport, middlePointerDown);
|
||
|
||
await waitFor(() => {
|
||
expect(viewport.className).toContain('image-canvas-editor__viewport--panning');
|
||
});
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
expect(
|
||
within(bottomToolbar)
|
||
.getByRole('button', { name: '选择工具' })
|
||
.getAttribute('aria-pressed'),
|
||
).toBe('true');
|
||
});
|
||
|
||
it('shows snap guides when dragging a layer near another layer alignment', async () => {
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
const puzzleLayer = screen.getByAltText('画布图片:拼图素材').closest('button')!;
|
||
dispatchPointerEvent(puzzleLayer, 'pointerdown', {
|
||
button: 0,
|
||
pointerId: 21,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
|
||
pointerId: 21,
|
||
clientX: 499,
|
||
clientY: 169,
|
||
});
|
||
|
||
expect(screen.getByTestId('image-canvas-editor-snap-guide-vertical')).toBeTruthy();
|
||
expect(screen.getByTestId('image-canvas-editor-snap-guide-horizontal')).toBeTruthy();
|
||
});
|
||
|
||
it('can switch tools after a layer drag started without pointer release', async () => {
|
||
const user = userEvent.setup();
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.pointerDown(screen.getByAltText('画布图片:拼图素材').closest('button')!, {
|
||
button: 0,
|
||
pointerId: 41,
|
||
clientX: 120,
|
||
clientY: 120,
|
||
});
|
||
fireEvent.pointerMove(screen.getByLabelText('画布工作区'), {
|
||
pointerId: 41,
|
||
clientX: 220,
|
||
clientY: 160,
|
||
});
|
||
|
||
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
|
||
const textTool = within(bottomToolbar).getByRole('button', { name: '文字工具' });
|
||
await user.click(textTool);
|
||
|
||
expect(textTool.getAttribute('aria-pressed')).toBe('true');
|
||
expect(screen.queryByTestId('image-canvas-editor-snap-guide-vertical')).toBeNull();
|
||
});
|
||
|
||
it('opens generated image metadata from the corner button and creates a real right-side edit result', async () => {
|
||
generateEditorImageMock.mockResolvedValueOnce({
|
||
imageSrc: 'data:image/png;base64,ZmFrZS1pbWFnZQ==',
|
||
width: 1024,
|
||
height: 1024,
|
||
sourceType: 'generated',
|
||
prompt: '一张可修改的生成图',
|
||
actualPrompt: '一张可修改的生成图',
|
||
model: 'gpt-image-2',
|
||
provider: 'VectorEngine',
|
||
taskId: 'editor-real-task-2',
|
||
});
|
||
editEditorImageMock.mockResolvedValueOnce({
|
||
imageSrc: 'data:image/png;base64,ZWRpdGVkLWltYWdl',
|
||
width: 1024,
|
||
height: 1024,
|
||
sourceType: 'generated',
|
||
prompt: '把画面改成黄昏光线',
|
||
actualPrompt: '把画面改成黄昏光线',
|
||
model: 'gpt-image-2',
|
||
provider: 'VectorEngine',
|
||
taskId: 'editor-real-edit-1',
|
||
});
|
||
render(<ImageCanvasEditorView />);
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||
target: { value: '一张可修改的生成图' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||
});
|
||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||
|
||
const metadataCornerButton = screen.getAllByRole('button', {
|
||
name: /查看生成图片 .*元数据/,
|
||
})[0];
|
||
if (!metadataCornerButton) {
|
||
throw new Error('metadata corner button should exist');
|
||
}
|
||
expect(metadataCornerButton.className).toContain('bg-black/55');
|
||
expect(metadataCornerButton.className).toContain(
|
||
'image-canvas-editor__metadata-corner',
|
||
);
|
||
fireEvent.click(metadataCornerButton);
|
||
|
||
const metadataDialog = screen.getByRole('dialog', { name: /生成图片 .*元数据/ });
|
||
expect(metadataDialog).toBeTruthy();
|
||
expect(within(metadataDialog).getByText('gpt-image-2')).toBeTruthy();
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '修改图片' }));
|
||
const editDialog = screen.getByRole('dialog', { name: '修改图片' });
|
||
expect(editDialog).toBeTruthy();
|
||
const editPrompt = screen.getByLabelText('生成提示词');
|
||
expect(editPrompt.className).toContain('platform-text-field');
|
||
expect(editPrompt.className).toContain('image-canvas-editor__generate-prompt');
|
||
fireEvent.change(editPrompt, {
|
||
target: { value: '把画面改成黄昏光线' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '修改' }));
|
||
|
||
expect(screen.getByRole('status').textContent).toContain('修改中');
|
||
expect(editEditorImageMock).toHaveBeenCalledWith({
|
||
prompt: '把画面改成黄昏光线',
|
||
sourceImageSrc: 'data:image/png;base64,ZmFrZS1pbWFnZQ==',
|
||
});
|
||
|
||
await waitFor(() => {
|
||
expect(screen.queryByRole('dialog', { name: '修改图片' })).toBeNull();
|
||
});
|
||
expect(screen.getByAltText(/画布图片:生成图片 .* 修改结果/)).toBeTruthy();
|
||
expect(screen.getByRole('button', { name: '当前缩放比例 100%' })).toBeTruthy();
|
||
});
|
||
});
|