Files
Genarrative/src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx
T
k88936 d1562ab65f
Project CI / Repository checks (pull_request) Successful in 1m35s
Project CI / Frontend tests (pull_request) Successful in 3m18s
Project CI / Backend tests (pull_request) Successful in 4m14s
Project CI / Native shell tests (pull_request) Successful in 16m18s
合并主分支并适配图标精灵图编辑
合入统一 Lexical 文本编辑器与滚动条样式调整
保留图标规范字段优化、字符限制与提交门禁
补齐图标生成及画布集成测试的 Lexical 交互适配
2026-08-08 15:23:29 +08:00

292 lines
9.8 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 { EDITOR_ICON_DESCRIPTION_MAX_CHARS } from '../../services/image-editor/editorProjectClient';
import {
getPlainTextEditorHost,
setPlainTextEditorValue,
} from '../common/AutoGrowTextArea.test-utils';
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('uses a grey hint instead of default icon text for new icon drafts', () => {
render(
<IconComposerHarness
initialDialog={createIconDialog({ iconDescriptions: [] })}
/>,
);
const input = screen.getByLabelText('素材描述');
expect(input.textContent).toBe('');
expect(
getPlainTextEditorHost(input).querySelector(
'.auto-grow-text-area__placeholder',
)?.textContent,
).toBe('你需要哪些图标?');
});
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('uploads and removes icon generation references', () => {
const requestUpload = vi.fn();
render(
<IconComposerHarness
initialDialog={createIconDialog({
generationReferences: [
{
id: 'ref-a',
label: '按钮风格参考',
src: 'data:image/png;base64,ref',
},
],
})}
onRequestUpload={requestUpload}
/>,
);
expect(screen.getByLabelText('按钮风格参考')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '上传图标素材参考图' }));
expect(requestUpload).toHaveBeenCalledWith('generation-reference');
fireEvent.click(screen.getByRole('button', { name: '删除按钮风格参考' }));
expect(screen.queryByLabelText('按钮风格参考')).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}
/>,
);
const description = screen.getByLabelText('素材描述');
expect(description.className).toContain('auto-grow-text-area');
expect(description.className).not.toContain('platform-text-field');
setPlainTextEditorValue(description, '魔法剑\n圆盾');
fireEvent.click(screen.getByRole('button', { name: '生成' }));
expect(updateIconDescriptionText).toHaveBeenCalledWith('魔法剑\n圆盾');
expect(submitIconGeneration).toHaveBeenCalledWith(dialog);
});
it('limits description input by Unicode characters', () => {
const updateIconDescriptionText = vi.fn();
render(
<IconComposerHarness
initialDialog={createIconDialog({ prompt: '' })}
onUpdateIconDescriptionText={updateIconDescriptionText}
/>,
);
setPlainTextEditorValue(
screen.getByLabelText('素材描述'),
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS + 1),
);
expect(updateIconDescriptionText).toHaveBeenCalledWith(
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
);
});
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');
expect(screen.getByRole('button', { name: '生成' }).textContent).toBe(
'生成3泥点',
);
});
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('素材描述').getAttribute('aria-disabled'),
).toBe('true');
expect(submitIconGeneration).not.toHaveBeenCalled();
rerender(
<IconComposerHarness
key="failed"
initialDialog={createIconDialog({
status: 'failed',
errorMessage: '生成失败',
})}
/>,
);
expect(screen.getByRole('alert').textContent).toContain('生成失败');
});
});