ea29887d9c
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交 - 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示 - 快速编辑提交前同步改写原图引用为实际图序号 - 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象 - 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
220 lines
7.5 KiB
TypeScript
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('生成失败');
|
|
});
|
|
});
|