调整图片画布路由和画布数据模型
将图片画布入口改为 /editor/canvas 新增 editor_canvas 表并关联 editor_project 默认画布 更新 project API 响应中的 canvas 快照兼容层 统一图片画布侧栏列表项和图标按钮组件 同步前端测试、SpacetimeDB bindings、技术文档和 TRACKING 记录
This commit is contained in:
@@ -284,7 +284,27 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.getByRole('button', { name: '当前缩放比例 50%' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('opens a generation dialog before creating a generated layer', async () => {
|
||||
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: '画布背景色' })).toBeTruthy();
|
||||
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('opens a canvas generation frame and composer before creating a generated layer', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce({
|
||||
imageSrc: 'data:image/png;base64,ZmFrZS1pbWFnZQ==',
|
||||
width: 1024,
|
||||
@@ -302,12 +322,19 @@ describe('ImageCanvasEditorView', () => {
|
||||
fireEvent.click(within(bottomToolbar).getByRole('button', { name: '生成工具' }));
|
||||
|
||||
const generateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||||
expect(generateDialog).toBeTruthy();
|
||||
const initialComposerTop = Number.parseFloat(
|
||||
(generateDialog as HTMLElement).style.top,
|
||||
);
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
expect(within(generateDialog).getByText('参考图')).toBeTruthy();
|
||||
expect(within(generateDialog).getByRole('button', { name: '生成比例 1:1 2k 1张' })).toBeTruthy();
|
||||
expect(within(generateDialog).getByRole('button', { name: '生成模型 GPT Image' })).toBeTruthy();
|
||||
expect(screen.queryByRole('toolbar', { name: 'AI画布工具栏' })).toBeNull();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张明亮的拼图主视觉' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
fireEvent.click(within(generateDialog).getByRole('button', { name: '生成' }));
|
||||
|
||||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||
@@ -315,15 +342,113 @@ describe('ImageCanvasEditorView', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
const anchoredGenerateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||||
expect(anchoredGenerateDialog).toBeTruthy();
|
||||
expect(
|
||||
Number.parseFloat((anchoredGenerateDialog as HTMLElement).style.top),
|
||||
).toBeCloseTo(initialComposerTop, 1);
|
||||
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 />);
|
||||
|
||||
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),
|
||||
).toBeCloseTo(draggedComposerTop, 1);
|
||||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||||
expect(Number.parseFloat((generatedLayer as HTMLElement).style.left)).toBeGreaterThan(700);
|
||||
expect(Number.parseFloat((generatedLayer as HTMLElement).style.top)).toBeGreaterThan(150);
|
||||
});
|
||||
|
||||
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 />);
|
||||
@@ -339,6 +464,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert').textContent).toContain('VectorEngine 未配置');
|
||||
});
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
expect(screen.queryByAltText(/画布图片:生成图片/)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -505,8 +631,9 @@ describe('ImageCanvasEditorView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
|
||||
const metadataCornerButton = screen.getAllByRole('button', {
|
||||
name: /查看生成图片 .*元数据/,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user