新增图片画布编辑器
新增 /editor 图片画布入口与 Lovart 风格画布交互 新增图片画布工程和资源持久化的 SpacetimeDB 表、绑定与 api-server BFF 接入图片生成和修改的 VectorEngine gpt-image-2 后端通道 完善素材库文件夹、重命名、上传删除、图层和元数据交互 补充图片画布技术方案、领域词、执行跟踪和浏览器 smoke 截图
This commit is contained in:
518
src/components/image-editor/ImageCanvasEditorView.test.tsx
Normal file
518
src/components/image-editor/ImageCanvasEditorView.test.tsx
Normal file
@@ -0,0 +1,518 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ImageCanvasEditorView } from './ImageCanvasEditorView';
|
||||
|
||||
const generateEditorImageMock = vi.hoisted(() => vi.fn());
|
||||
const editEditorImageMock = 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,
|
||||
generateEditorImage: generateEditorImageMock,
|
||||
};
|
||||
});
|
||||
|
||||
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', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
generateEditorImageMock.mockReset();
|
||||
editEditorImageMock.mockReset();
|
||||
});
|
||||
|
||||
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('重命名素材拼图素材');
|
||||
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: '新建素材文件夹' }));
|
||||
await user.type(screen.getByLabelText('素材文件夹名称'), '角色上传');
|
||||
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('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')!);
|
||||
|
||||
expect(screen.getByText('420 x 420 px')).toBeTruthy();
|
||||
|
||||
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 () => {
|
||||
const createObjectUrlSpy = vi.fn(() => 'blob:uploaded-image');
|
||||
Object.defineProperty(URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: createObjectUrlSpy,
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
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' }),
|
||||
);
|
||||
|
||||
expect(createObjectUrlSpy).toHaveBeenCalled();
|
||||
expect(screen.getByAltText('画布图片:测试上传.png')).toBeTruthy();
|
||||
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%' })).toBeTruthy();
|
||||
|
||||
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('opens a generation dialog 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: '生成图片' });
|
||||
expect(generateDialog).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张明亮的拼图主视觉' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||
prompt: '一张明亮的拼图主视觉',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
});
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
const metadataButtons = screen.getAllByRole('button', {
|
||||
name: /查看生成图片 .*元数据/,
|
||||
});
|
||||
expect(metadataButtons[0]).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.queryByAltText(/画布图片:生成图片/)).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.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
});
|
||||
|
||||
const metadataCornerButton = screen.getAllByRole('button', {
|
||||
name: /查看生成图片 .*元数据/,
|
||||
})[0];
|
||||
if (!metadataCornerButton) {
|
||||
throw new Error('metadata corner button should exist');
|
||||
}
|
||||
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();
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
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();
|
||||
});
|
||||
});
|
||||
1951
src/components/image-editor/ImageCanvasEditorView.tsx
Normal file
1951
src/components/image-editor/ImageCanvasEditorView.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user