修复图片画布新增素材持久化

新增画布图层资源创建后的即时布局保存
补充素材库图片加入画布的持久化回归测试
更新图片画布回归验证记录
This commit is contained in:
2026-06-17 04:42:09 +08:00
parent b5cbe62b47
commit f794a8dd1f
4 changed files with 276 additions and 132 deletions

View File

@@ -242,26 +242,26 @@ describe('ImageCanvasEditorView', () => {
beforeEach(() => {
loadOrCreateRecentEditorProjectMock.mockImplementation(() =>
immediateAsync({
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
}),
);
loadEditorAssetLibraryMock.mockImplementation(() =>
immediateAsync({
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: defaultEditorAssetLibraryAssets,
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: defaultEditorAssetLibraryAssets,
}),
);
createEditorAssetMock.mockImplementation(async (input) => ({
@@ -378,7 +378,9 @@ describe('ImageCanvasEditorView', () => {
it('shows the loaded project title and a topbar entry back to projects', async () => {
render(<ImageCanvasEditorView />);
expect(await screen.findByRole('heading', { name: '默认项目' })).toBeTruthy();
expect(
await screen.findByRole('heading', { name: '默认项目' }),
).toBeTruthy();
const projectLink = screen.getByRole('link', { name: '返回项目页面' });
expect(projectLink.getAttribute('href')).toBe('/project');
@@ -422,7 +424,9 @@ describe('ImageCanvasEditorView', () => {
'新画布项目',
);
});
expect(await screen.findByRole('heading', { name: '新画布项目' })).toBeTruthy();
expect(
await screen.findByRole('heading', { name: '新画布项目' }),
).toBeTruthy();
});
it('does not inject built-in mock assets when the persisted library is empty', async () => {
@@ -974,7 +978,9 @@ describe('ImageCanvasEditorView', () => {
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(createEditorAssetMock).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '上传失败登录后上传.png' })).toBeNull();
expect(
screen.queryByRole('button', { name: '上传失败登录后上传.png' }),
).toBeNull();
const resumeUpload = openLoginModal.mock.calls[0]?.[0];
expect(typeof resumeUpload).toBe('function');
@@ -1027,7 +1033,9 @@ describe('ImageCanvasEditorView', () => {
expect(
await screen.findByLabelText('素材素材上传进度.png上传进度'),
).toBeTruthy();
expect(screen.getByRole('button', { name: '上传中素材上传进度.png' })).toBeTruthy();
expect(
screen.getByRole('button', { name: '上传中素材上传进度.png' }),
).toBeTruthy();
deferredAsset.resolve({
assetId: 'asset-upload-progress',
@@ -1044,9 +1052,7 @@ describe('ImageCanvasEditorView', () => {
screen.getByRole('button', { name: '添加素材上传进度.png' }),
).toBeTruthy();
});
expect(
screen.queryByLabelText('素材素材上传进度.png上传进度'),
).toBeNull();
expect(screen.queryByLabelText('素材素材上传进度.png上传进度')).toBeNull();
});
it('opens login when asset creation returns unauthorized during upload', async () => {
@@ -1212,6 +1218,69 @@ describe('ImageCanvasEditorView', () => {
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-b');
});
it('saves a library asset layer right after creating its canvas resource', async () => {
const user = userEvent.setup();
createEditorProjectResourceMock.mockResolvedValueOnce({
resourceId: 'resource-added-asset-a',
projectId: 'editor-project-default',
imageSrc: 'data:image/png;base64,YQ==',
width: 320,
height: 240,
sourceType: 'uploaded',
});
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
projectId: 'editor-project-default',
title: '空画布项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
});
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',
},
],
});
render(<ImageCanvasEditorView />);
await user.click(
await screen.findByRole('button', { name: '添加账号素材A' }),
);
expect(await screen.findByAltText('画布图片账号素材A')).toBeTruthy();
await waitFor(() => {
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
'editor-project-default',
expect.objectContaining({
layers: expect.arrayContaining([
expect.objectContaining({
title: '账号素材A',
resourceId: 'resource-added-asset-a',
sourceAssetId: 'asset-a',
}),
]),
}),
);
});
});
it('selects multiple assets with a marquee in asset selection mode', async () => {
const user = userEvent.setup();
loadEditorAssetLibraryMock.mockResolvedValueOnce({
@@ -1601,8 +1670,11 @@ describe('ImageCanvasEditorView', () => {
const menu = screen.getByRole('menu', { name: '画布右键菜单' });
expect(
(within(menu).getByRole('menuitem', { name: '粘贴' }) as HTMLButtonElement)
.disabled,
(
within(menu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(within(menu).getByRole('menuitem', { name: '放大' })).toBeTruthy();
expect(
@@ -1656,11 +1728,15 @@ describe('ImageCanvasEditorView', () => {
});
const copyPasteMenu = screen.getByRole('menu', { name: '画布右键菜单' });
expect(
(within(copyPasteMenu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement).disabled,
(
within(copyPasteMenu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement
).disabled,
).toBe(false);
fireEvent.click(within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }));
fireEvent.click(
within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }),
);
expect(screen.getAllByAltText(//u)).toHaveLength(2);
fireEvent.contextMenu(
@@ -1885,7 +1961,9 @@ describe('ImageCanvasEditorView', () => {
).toBeTruthy();
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至100%' }));
expect(screen.getByRole('button', { name: / \d+%/u })).toBeTruthy();
expect(
screen.getByRole('button', { name: / \d+%/u }),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '当前缩放比例 100%' }));
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' }));
@@ -1915,25 +1993,23 @@ describe('ImageCanvasEditorView', () => {
});
expect(within(settingsPanel).getByText('画布背景')).toBeTruthy();
fireEvent.click(within(settingsPanel).getByRole('button', { name: '暖灰' }));
fireEvent.click(
within(settingsPanel).getByRole('button', { name: '暖灰' }),
);
expect((viewport as HTMLElement).style.backgroundColor).toBe(
'rgb(243, 240, 234)',
);
fireEvent.change(
within(settingsPanel).getByLabelText('自定义画布背景色'),
{
target: { value: '#ffffff' },
},
);
fireEvent.change(within(settingsPanel).getByLabelText('自定义画布背景色'), {
target: { value: '#ffffff' },
});
expect((viewport as HTMLElement).style.backgroundColor).toBe(
'rgb(255, 255, 255)',
);
const hexInput = within(settingsPanel).getByLabelText(
'画布背景十六进制颜色',
);
const hexInput =
within(settingsPanel).getByLabelText('画布背景十六进制颜色');
fireEvent.change(hexInput, { target: { value: '#abc' } });
expect((hexInput as HTMLInputElement).value).toBe('#aabbcc');
expect((viewport as HTMLElement).style.backgroundColor).toBe(
@@ -1954,9 +2030,7 @@ describe('ImageCanvasEditorView', () => {
);
fireEvent.keyDown(window, { key: 'Escape' });
expect(
screen.queryByRole('dialog', { name: '画布背景设置' }),
).toBeNull();
expect(screen.queryByRole('dialog', { name: '画布背景设置' })).toBeNull();
fireEvent.click(
within(panelToolbar).getByRole('button', { name: '切换小地图' }),
@@ -2927,7 +3001,9 @@ describe('ImageCanvasEditorView', () => {
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
target: { value: '高个子游侠' },
});
fireEvent.click(within(characterPanel).getByRole('button', { name: '生成' }));
fireEvent.click(
within(characterPanel).getByRole('button', { name: '生成' }),
);
await waitFor(() => {
expect(generateEditorImageMock).toHaveBeenCalledWith(
@@ -2937,9 +3013,7 @@ describe('ImageCanvasEditorView', () => {
}),
);
});
expect(
generateEditorImageMock.mock.calls[0]?.[0],
).not.toEqual(
expect(generateEditorImageMock.mock.calls[0]?.[0]).not.toEqual(
expect.objectContaining({
aspectRatio: expect.any(String),
imageSize: expect.any(String),
@@ -3019,12 +3093,16 @@ describe('ImageCanvasEditorView', () => {
const characterFrame = screen.getByLabelText('角色生成占位图');
expect(characterFrame).toBeTruthy();
dispatchPointerEvent(screen.getByLabelText('图像生成占位图'), 'pointerdown', {
button: 0,
pointerId: 1702,
clientX: 500,
clientY: 260,
});
dispatchPointerEvent(
screen.getByLabelText('图像生成占位图'),
'pointerdown',
{
button: 0,
pointerId: 1702,
clientX: 500,
clientY: 260,
},
);
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
pointerId: 1702,
clientX: 650,
@@ -3036,9 +3114,7 @@ describe('ImageCanvasEditorView', () => {
clientY: 390,
});
const movedFrame = screen.getByLabelText('图像生成占位图');
const movedLeft = Number.parseFloat(
(movedFrame as HTMLElement).style.left,
);
const movedLeft = Number.parseFloat((movedFrame as HTMLElement).style.left);
const movedTop = Number.parseFloat((movedFrame as HTMLElement).style.top);
expect(movedLeft).toBeGreaterThan(originalLeft);
expect(movedTop).toBeGreaterThan(originalTop);
@@ -3077,9 +3153,13 @@ describe('ImageCanvasEditorView', () => {
.getByAltText(/画布图片:生成图片/)
.closest('button') as HTMLElement;
const expectedLayerLeft =
movedLeft + Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 - 512;
movedLeft +
Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 -
512;
const expectedLayerTop =
movedTop + Number.parseFloat((movedFrame as HTMLElement).style.height) / 2 - 512;
movedTop +
Number.parseFloat((movedFrame as HTMLElement).style.height) / 2 -
512;
expect(Number.parseFloat(generatedLayer.style.left)).toBeCloseTo(
expectedLayerLeft,
1,
@@ -3160,7 +3240,9 @@ describe('ImageCanvasEditorView', () => {
iconPanel.querySelector('.image-canvas-editor__icon-spec-card'),
).toBeTruthy();
fireEvent.click(within(iconPanel).getByRole('button', { name: '添加素材描述' }));
fireEvent.click(
within(iconPanel).getByRole('button', { name: '添加素材描述' }),
);
expect(Number.parseFloat(iconPanel.style.width)).toBeCloseTo(61.2, 1);
expect(within(iconPanel).getAllByRole('textbox')).toHaveLength(7);
@@ -4358,12 +4440,16 @@ describe('ImageCanvasEditorView', () => {
});
expect(within(editedMetadataDialog).queryByText('Prompt')).toBeNull();
expect(within(editedMetadataDialog).getByText('修改要求')).toBeTruthy();
expect(within(editedMetadataDialog).getByText('把画面改成黄昏光线')).toBeTruthy();
expect(
within(editedMetadataDialog).getByText('把画面改成黄昏光线'),
).toBeTruthy();
expect(within(editedMetadataDialog).getByText('参考图')).toBeTruthy();
expect(
within(editedMetadataDialog).getByText(/^ \d+$/u),
).toBeTruthy();
expect(screen.getByRole('button', { name: / \d+%/u })).toBeTruthy();
expect(
screen.getByRole('button', { name: / \d+%/u }),
).toBeTruthy();
});
it('hides the edit image panel after generation starts while keeping the source preview visible', async () => {