修复画布素材交互缺口
统一默认素材文件夹,避免侧栏拖拽上传重复生成图片 区分素材入库和画布拖拽上传,画布落点增加安全兜底 补齐画布 Shift 多选、框选渲染和多图层打组能力 调整生成器对话框隐藏逻辑,关闭按钮保留占位图 将缩放比例入口放入左下角面板并拦截编辑器内 Ctrl 滚轮缩放页面 补充素材上传、画布多选、图层打组和生成器隐藏回归测试
This commit is contained in:
@@ -200,7 +200,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: '图片资源栏' });
|
||||
expect(within(sidebar).getByRole('region', { name: '项目素材' })).toBeTruthy();
|
||||
expect(within(sidebar).getByRole('region', { name: '参考素材' })).toBeTruthy();
|
||||
expect(within(sidebar).queryByRole('region', { name: '参考素材' })).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '重命名素材拼图素材' }));
|
||||
const renameInput = screen.getByLabelText('重命名素材拼图素材');
|
||||
@@ -248,7 +248,6 @@ describe('ImageCanvasEditorView', () => {
|
||||
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();
|
||||
@@ -256,7 +255,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
await user.click(within(customFolder).getByRole('button', { name: '删除素材角色草图.png' }));
|
||||
|
||||
expect(screen.queryByRole('button', { name: '添加角色草图.png' })).toBeNull();
|
||||
expect(screen.getByAltText('画布图片:角色草图.png')).toBeTruthy();
|
||||
expect(screen.queryByAltText('画布图片:角色草图.png')).toBeNull();
|
||||
});
|
||||
|
||||
it('renames and deletes asset folders through the persisted asset library API', async () => {
|
||||
@@ -302,21 +301,21 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(deleteEditorAssetFolderMock).toHaveBeenCalledWith('folder-role');
|
||||
});
|
||||
|
||||
it('uploads multiple files and persists them as account-level assets', async () => {
|
||||
it('uploads multiple files as account-level assets without adding canvas layers', 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(screen.getByRole('button', { name: '添加第一张.png' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '添加第二张.png' })).toBeTruthy();
|
||||
});
|
||||
expect(createEditorAssetMock).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByAltText('画布图片:第一张.png')).toBeNull();
|
||||
expect(screen.queryByAltText('画布图片:第二张.png')).toBeNull();
|
||||
});
|
||||
|
||||
it('supports asset selection mode and batch delete with shared toolbar', async () => {
|
||||
@@ -534,21 +533,21 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.getByAltText('画布图片:大鱼素材')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uploads an image file as a new canvas layer', async () => {
|
||||
it('drops an image file on the canvas 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' }),
|
||||
);
|
||||
const viewport = screen.getByLabelText('画布工作区');
|
||||
fireEvent.drop(viewport, {
|
||||
clientX: 430,
|
||||
clientY: 260,
|
||||
dataTransfer: {
|
||||
files: [new File(['image'], '测试上传.png', { type: 'image/png' })],
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText('画布图片:测试上传.png')).toBeTruthy();
|
||||
@@ -562,6 +561,23 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.getByRole('button', { name: '选择图层测试上传.png' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('drops files into the asset panel only once without creating canvas layers', async () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.drop(screen.getByRole('region', { name: '项目素材' }), {
|
||||
dataTransfer: {
|
||||
files: [new File(['image'], '素材拖拽.png', { type: 'image/png' })],
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: '添加素材拖拽.png' })).toBeTruthy();
|
||||
});
|
||||
expect(createEditorAssetMock).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByAltText('画布图片:素材拖拽.png')).toBeNull();
|
||||
});
|
||||
|
||||
it('blocks the browser context menu inside the editor workspace', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
@@ -674,6 +690,64 @@ describe('ImageCanvasEditorView', () => {
|
||||
clientY: 280,
|
||||
});
|
||||
expect(screen.getByRole('button', { name: '当前缩放比例 90%' })).toBeTruthy();
|
||||
|
||||
const ctrlWheelEvent = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey: true,
|
||||
deltaY: -120,
|
||||
clientX: 400,
|
||||
clientY: 280,
|
||||
});
|
||||
viewport.dispatchEvent(ctrlWheelEvent);
|
||||
expect(ctrlWheelEvent.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('selects multiple canvas layers with shift click', async () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
const firstLayer = screen.getByAltText('画布图片:拼图素材').closest('button')!;
|
||||
const secondLayer = screen.getByAltText('画布图片:大鱼素材').closest('button')!;
|
||||
|
||||
fireEvent.pointerDown(firstLayer, {
|
||||
button: 0,
|
||||
pointerId: 81,
|
||||
clientX: 120,
|
||||
clientY: 120,
|
||||
});
|
||||
fireEvent.pointerUp(screen.getByLabelText('画布工作区'), {
|
||||
pointerId: 81,
|
||||
clientX: 120,
|
||||
clientY: 120,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByAltText('画布图片:拼图素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
});
|
||||
fireEvent.keyDown(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||
fireEvent.pointerDown(secondLayer, {
|
||||
button: 0,
|
||||
pointerId: 82,
|
||||
clientX: 520,
|
||||
clientY: 180,
|
||||
shiftKey: true,
|
||||
});
|
||||
fireEvent.pointerUp(screen.getByLabelText('画布工作区'), {
|
||||
pointerId: 82,
|
||||
clientX: 520,
|
||||
clientY: 180,
|
||||
});
|
||||
fireEvent.keyUp(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByAltText('画布图片:拼图素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
expect(
|
||||
screen.getByAltText('画布图片:大鱼素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
});
|
||||
});
|
||||
|
||||
it('drags the minimap to move the canvas viewport', () => {
|
||||
@@ -707,10 +781,48 @@ describe('ImageCanvasEditorView', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图层' }));
|
||||
fireEvent.pointerDown(screen.getByAltText('画布图片:拼图素材').closest('button')!, {
|
||||
button: 0,
|
||||
pointerId: 90,
|
||||
clientX: 120,
|
||||
clientY: 120,
|
||||
});
|
||||
fireEvent.pointerUp(screen.getByLabelText('画布工作区'), {
|
||||
pointerId: 90,
|
||||
clientX: 120,
|
||||
clientY: 120,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByAltText('画布图片:拼图素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
});
|
||||
fireEvent.keyDown(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||
fireEvent.pointerDown(screen.getByAltText('画布图片:大鱼素材').closest('button')!, {
|
||||
button: 0,
|
||||
pointerId: 91,
|
||||
clientX: 520,
|
||||
clientY: 180,
|
||||
shiftKey: true,
|
||||
});
|
||||
fireEvent.pointerUp(screen.getByLabelText('画布工作区'), {
|
||||
pointerId: 91,
|
||||
clientX: 520,
|
||||
clientY: 180,
|
||||
});
|
||||
fireEvent.keyUp(window, { key: 'Shift', code: 'ShiftLeft' });
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByAltText('画布图片:拼图素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
expect(
|
||||
screen.getByAltText('画布图片:大鱼素材').closest('button')?.className,
|
||||
).toContain('image-canvas-editor__layer--selected');
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '图层打组' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/已打组/u)).toBeTruthy();
|
||||
expect(screen.getAllByText(/已打组/u)).toHaveLength(2);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
||||
@@ -721,6 +833,10 @@ describe('ImageCanvasEditorView', () => {
|
||||
title: '拼图素材',
|
||||
groupId: expect.stringMatching(/^layer-group-/u),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: '大鱼素材',
|
||||
groupId: expect.stringMatching(/^layer-group-/u),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
@@ -870,7 +986,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(Number.parseFloat((generatedLayer as HTMLElement).style.top)).toBeGreaterThan(180);
|
||||
});
|
||||
|
||||
it('keeps the generation composer when selecting another image', () => {
|
||||
it('hides the generation composer when selecting another image but keeps the placeholder', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||||
@@ -883,8 +999,17 @@ describe('ImageCanvasEditorView', () => {
|
||||
clientY: 120,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
|
||||
fireEvent.pointerDown(screen.getByLabelText('图像生成占位图'), {
|
||||
button: 0,
|
||||
pointerId: 64,
|
||||
clientX: 300,
|
||||
clientY: 180,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps the generation composer when clicking the canvas outside generation controls', () => {
|
||||
@@ -904,6 +1029,16 @@ describe('ImageCanvasEditorView', () => {
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('closes the generation composer without removing the placeholder frame', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' }));
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.getByLabelText('图像生成占位图')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows generation errors instead of falling back to mock images', async () => {
|
||||
generateEditorImageMock.mockRejectedValueOnce(new Error('VectorEngine 未配置'));
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3487,6 +3487,14 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__canvas-marquee {
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
border: 1px solid #2563eb;
|
||||
background: rgb(37 99 235 / 0.12);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-button {
|
||||
display: block;
|
||||
border: 0;
|
||||
@@ -5222,6 +5230,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
}
|
||||
|
||||
.image-canvas-editor__panel-dock .image-canvas-editor__zoom-trigger {
|
||||
width: auto;
|
||||
min-width: 4.15rem;
|
||||
padding: 0 0.65rem;
|
||||
}
|
||||
|
||||
@keyframes baby-object-gift-lid-open {
|
||||
0% {
|
||||
transform: rotate(0deg) translateY(0);
|
||||
|
||||
Reference in New Issue
Block a user