Files
Genarrative/src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx
T
JenkenB ea29887d9c 完善快速编辑参考图与生成占位
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交

- 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示

- 快速编辑提交前同步改写原图引用为实际图序号

- 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象

- 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
2026-06-18 22:50:42 +08:00

220 lines
7.5 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { createRef, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';
import type {
GenerateDialogState,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
function createIconDialog(
patch: Partial<GenerateDialogState> = {},
): GenerateDialogState {
return {
mode: 'icon',
prompt: '',
status: 'idle',
iconDescriptions: ['剑', '盾'],
imageModel: 'nanobanana2',
aspectRatio: '1:1',
imageSize: '1K',
...patch,
};
}
function IconComposerHarness({
initialDialog,
initialMenuOpen = false,
onOpenSpecDialog = vi.fn(),
onRequestUpload = vi.fn(),
onUpdateIconDescriptionText = vi.fn(),
onRememberImageModel = vi.fn(),
onSubmit = vi.fn(),
}: {
initialDialog: GenerateDialogState;
initialMenuOpen?: boolean;
onOpenSpecDialog?: (specType: 'character' | 'ui' | 'icon' | 'custom') => void;
onRequestUpload?: (target: UploadTarget) => void;
onUpdateIconDescriptionText?: (value: string) => void;
onRememberImageModel?: (model: string) => void;
onSubmit?: (dialog: GenerateDialogState) => void;
}) {
const [dialog, setDialog] = useState<GenerateDialogState | null>(
initialDialog,
);
const [isMenuOpen, setIsMenuOpen] = useState(initialMenuOpen);
const [isPickingIconSpec, setIsPickingIconSpec] = useState(false);
const iconSpecButtonRef = createRef<HTMLButtonElement>();
return dialog ? (
<div>
<ImageCanvasIconSpritesheetComposerView
dialog={dialog}
style={{ left: 12, top: 24 }}
iconSpecButtonRef={iconSpecButtonRef}
isIconSpecMenuOpen={isMenuOpen}
setGenerateDialog={setDialog}
setIsIconSpecMenuOpen={setIsMenuOpen}
setIsPickingIconSpecFromCanvas={setIsPickingIconSpec}
renderEditorPortal={(node) => node}
buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })}
onOpenSpecDialog={onOpenSpecDialog}
onRequestUpload={onRequestUpload}
onUpdateIconDescriptionText={onUpdateIconDescriptionText}
onRememberImageModel={onRememberImageModel}
onSubmit={onSubmit}
/>
<output aria-label="当前菜单">{String(isMenuOpen)}</output>
<output aria-label="正在选择图标规范">{String(isPickingIconSpec)}</output>
<output aria-label="当前模型">{dialog.imageModel}</output>
<output aria-label="当前尺寸">{dialog.imageSize}</output>
</div>
) : null;
}
describe('ImageCanvasIconSpritesheetComposerView', () => {
it('opens icon spec menu and supports each source action', () => {
const openSpecDialog = vi.fn();
const requestUpload = vi.fn();
render(
<IconComposerHarness
initialDialog={createIconDialog()}
onOpenSpecDialog={openSpecDialog}
onRequestUpload={requestUpload}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '图标规范' }));
expect(screen.getByRole('menu', { name: '图标规范来源' })).toBeTruthy();
fireEvent.click(screen.getByRole('menuitem', { name: '从画布中选择' }));
expect(screen.getByLabelText('正在选择图标规范').textContent).toBe('true');
expect(screen.getByLabelText('当前菜单').textContent).toBe('false');
fireEvent.click(screen.getByRole('button', { name: '图标规范' }));
fireEvent.click(screen.getByRole('menuitem', { name: '新建图标规范' }));
expect(openSpecDialog).toHaveBeenCalledWith('icon');
fireEvent.click(screen.getByRole('button', { name: '图标规范' }));
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
expect(requestUpload).toHaveBeenCalledWith('icon-spec');
});
it('keeps the spec reference as a single first-row card without extra source buttons', () => {
render(<IconComposerHarness initialDialog={createIconDialog()} />);
const panel = screen.getByRole('dialog', { name: '生成图标素材' });
const firstRow = panel.querySelector('.image-canvas-editor__reference-strip');
expect(firstRow).toBeTruthy();
expect(
within(firstRow as HTMLElement).getByRole('button', {
name: '图标规范',
}),
).toBeTruthy();
expect(
panel.querySelector('.image-canvas-editor__icon-spec-actions'),
).toBeNull();
expect(screen.queryByRole('button', { name: '画布' })).toBeNull();
expect(screen.queryByRole('button', { name: '新建' })).toBeNull();
expect(screen.queryByRole('button', { name: '上传' })).toBeNull();
});
it('removes a bound icon spec from the hover delete button', () => {
render(
<IconComposerHarness
initialDialog={createIconDialog({
iconSpecReference: {
id: 'icon-spec-a',
label: '图标规范A',
src: 'data:image/png;base64,aWNvbg==',
},
})}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '删除图标规范' }));
expect(screen.getByRole('button', { name: '图标规范' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '图标规范A' })).toBeNull();
});
it('updates multi-line descriptions and submits', () => {
const updateIconDescriptionText = vi.fn();
const submitIconGeneration = vi.fn();
const dialog = createIconDialog({ prompt: '剑\n盾' });
render(
<IconComposerHarness
initialDialog={dialog}
onUpdateIconDescriptionText={updateIconDescriptionText}
onSubmit={submitIconGeneration}
/>,
);
fireEvent.change(screen.getByLabelText('素材描述'), {
target: { value: '魔法剑\n圆盾' },
});
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect(updateIconDescriptionText).toHaveBeenCalledWith('魔法剑\n圆盾');
expect(submitIconGeneration).toHaveBeenCalledWith(dialog);
});
it('keeps image option changes inside the icon dialog', () => {
const rememberImageModel = vi.fn();
render(
<IconComposerHarness
initialDialog={createIconDialog({
imageModel: 'nanobanana2',
imageSize: '0.5K',
})}
onRememberImageModel={rememberImageModel}
/>,
);
fireEvent.click(
screen.getByRole('button', { name: '生成图片模型 nanobanana2' }),
);
const modelPanel = screen.getByRole('menu', { name: '生成图片模型选项' });
fireEvent.click(
within(modelPanel).getByRole('button', { name: 'gpt-image-2' }),
);
expect(rememberImageModel).toHaveBeenCalledWith('gpt-image-2');
expect(screen.getByLabelText('当前模型').textContent).toBe('gpt-image-2');
expect(screen.getByLabelText('当前尺寸').textContent).toBe('1K');
});
it('disables generation controls and renders failure state', () => {
const submitIconGeneration = vi.fn();
const { rerender } = render(
<IconComposerHarness
initialDialog={createIconDialog({ status: 'generating' })}
onSubmit={submitIconGeneration}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect((screen.getByLabelText('素材描述') as HTMLInputElement).disabled).toBe(
true,
);
expect(submitIconGeneration).not.toHaveBeenCalled();
rerender(
<IconComposerHarness
key="failed"
initialDialog={createIconDialog({
status: 'failed',
errorMessage: '生成失败',
})}
/>,
);
expect(screen.getByRole('alert').textContent).toContain('生成失败');
});
});