/* @vitest-environment jsdom */ import { fireEvent, render, screen, within } from '@testing-library/react'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import type { GenerateDialogState } from './ImageCanvasEditorTypes'; import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView'; import { calculateEditorImageGenerationPrice, IMAGE_MODEL_GPT_IMAGE_2, IMAGE_MODEL_NANOBANANA2, } from './ImageCanvasGenerationModel'; function ImageOptionsHarness({ initialDialog, includeDimensions = true, hasPendingImageReferenceUploads = false, onRememberImageModel = vi.fn(), lockedModel, }: { initialDialog: GenerateDialogState; includeDimensions?: boolean; hasPendingImageReferenceUploads?: boolean; onRememberImageModel?: (model: string) => void; lockedModel?: string; }) { const [dialog, setDialog] = useState( initialDialog, ); return dialog ? (
undefined} /> {dialog.imageModel} {dialog.aspectRatio} {dialog.imageSize} {dialog.style ?? '-'} {dialog.status} {dialog.errorMessage ?? '-'} {dialog.mode === 'character' ? (dialog.characterReferences?.length ?? 0) : dialog.mode === 'publication' ? (dialog.publicationReferences?.length ?? 0) : (dialog.generationReferences?.length ?? 0)} {dialog.placeholder ? [ dialog.placeholder.x, dialog.placeholder.y, dialog.placeholder.width, dialog.placeholder.height, dialog.placeholder.originalWidth, dialog.placeholder.originalHeight, ].join(':') : '-'}
) : null; } describe('ImageCanvasGenerationImageOptionsView', () => { it.each(['generate', 'character', 'icon'] as const)( 'shows and updates the pixel art style for %s generation', (mode) => { render( , ); const toggle = screen.getByRole('checkbox', { name: '像素艺术', }) as HTMLInputElement; const modelButton = screen.getByRole('button', { name: '生成图片模型 nanobanana2', }); expect(toggle.checked).toBe(false); expect( toggle.compareDocumentPosition(modelButton) & Node.DOCUMENT_POSITION_FOLLOWING, ).not.toBe(0); fireEvent.click(toggle); expect(toggle.checked).toBe(true); expect(screen.getByLabelText('当前风格').textContent).toBe('pixelArt'); expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); expect(screen.getByLabelText('当前错误').textContent).toBe('-'); }, ); it.each(['spec', 'quick-edit', 'ui-design', 'publication'] as const)( 'does not show the pixel art style for %s generation', (mode) => { render( , ); expect(screen.queryByRole('checkbox', { name: '像素艺术' })).toBeNull(); }, ); it('updates dimensions from a menu, keeps the menu open and marks the selection', () => { render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }), ); const panel = screen.getByRole('menu', { name: '生成图片尺寸选项' }); fireEvent.click(within(panel).getByRole('button', { name: '16:9' })); fireEvent.click(within(panel).getByRole('button', { name: '2K' })); expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy(); expect( within(panel) .getByRole('button', { name: '16:9' }) .getAttribute('aria-pressed'), ).toBe('true'); expect( within(panel) .getByRole('button', { name: '2K' }) .getAttribute('aria-pressed'), ).toBe('true'); expect( screen.getByRole('button', { name: '生成图片尺寸 16:9·2K' }), ).toBeTruthy(); expect(screen.getByLabelText('当前比例').textContent).toBe('16:9'); expect(screen.getByLabelText('当前尺寸').textContent).toBe('2K'); expect(screen.getByLabelText('当前状态').textContent).toBe('idle'); expect(screen.getByLabelText('当前错误').textContent).toBe('-'); }); it('closes option panels when clicking parent panel outside trigger buttons', () => { render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }), ); expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy(); fireEvent.click(screen.getByLabelText('当前模型')); expect(screen.queryByRole('menu', { name: '生成图片尺寸选项' })).toBeNull(); }); it('renders model choices as one model per row and uses a check mark for selection', () => { const rememberImageModel = vi.fn(); render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片模型 nanobanana2' }), ); const panel = screen.getByRole('menu', { name: '生成图片模型选项' }); expect( within(panel).getByRole('button', { name: 'nanobanana2' }), ).toBeTruthy(); fireEvent.click(within(panel).getByRole('button', { name: 'gpt-image-2' })); expect(screen.getByRole('menu', { name: '生成图片模型选项' })).toBeTruthy(); expect(rememberImageModel).toHaveBeenCalledWith(IMAGE_MODEL_GPT_IMAGE_2); expect(screen.getByLabelText('当前模型').textContent).toBe( IMAGE_MODEL_GPT_IMAGE_2, ); expect(screen.getByLabelText('当前比例').textContent).toBe('9:16'); expect(screen.getByLabelText('当前尺寸').textContent).toBe('1K'); expect( screen.getByRole('button', { name: '生成图片模型 gpt-image-2' }), ).toBeTruthy(); expect( screen.getByRole('button', { name: '生成角色形象' }).textContent, ).toBe('生成3泥点'); }); it('locks the model trigger when a fixed model is provided', () => { const rememberImageModel = vi.fn(); render( , ); const modelButton = screen.getByRole('button', { name: '生成图片模型 gpt-image-2', }) as HTMLButtonElement; expect(modelButton.disabled).toBe(true); expect(modelButton.closest('div')?.className).toContain( 'image-canvas-editor__readonly-generation-option', ); expect(modelButton.textContent).not.toContain('nanobanana2'); fireEvent.click(modelButton); expect(screen.queryByRole('menu', { name: '生成图片模型选项' })).toBeNull(); expect(rememberImageModel).not.toHaveBeenCalled(); }); it('rejects a model switch that would discard existing references', () => { const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {}); const rememberImageModel = vi.fn(); render( ({ id: `reference-${index + 1}`, label: `参考图${index + 1}`, src: `/reference-${index + 1}.png`, })), }} includeDimensions={false} onRememberImageModel={rememberImageModel} />, ); expect(screen.getByLabelText('当前参考图数量').textContent).toBe('8'); fireEvent.click( screen.getByRole('button', { name: '生成图片模型 nanobanana2' }), ); fireEvent.click( within(screen.getByRole('menu', { name: '生成图片模型选项' })).getByRole( 'button', { name: 'gpt-image-2' }, ), ); expect(alertMock).toHaveBeenCalledWith( '当前已有 8 张参考图,gpt-image-2 最多允许 4 张,请先删除多余参考图后再切换', ); expect(screen.getByLabelText('当前参考图数量').textContent).toBe('8'); expect(screen.getByLabelText('当前模型').textContent).toBe( IMAGE_MODEL_NANOBANANA2, ); expect(rememberImageModel).not.toHaveBeenCalled(); }); it('rejects model switching while reference uploads are pending', () => { const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {}); const rememberImageModel = vi.fn(); render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片模型 nanobanana2' }), ); fireEvent.click( within(screen.getByRole('menu', { name: '生成图片模型选项' })).getByRole( 'button', { name: 'gpt-image-2' }, ), ); expect(alertMock).toHaveBeenCalledWith( '参考图正在上传,请等待上传完成后再切换模型', ); expect(screen.getByLabelText('当前模型').textContent).toBe( IMAGE_MODEL_NANOBANANA2, ); expect( ( screen.getByRole('button', { name: '生成角色形象', }) as HTMLButtonElement ).disabled, ).toBe(true); expect(rememberImageModel).not.toHaveBeenCalled(); }); it('renders option menus and shows mud point text in the submit button', () => { render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }), ); expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy(); const submit = screen.getByRole('button', { name: '生成角色形象' }); expect(submit.textContent).toBe('生成12泥点'); }); it('keeps fixed-card placeholders unchanged when model-only controls are shown', () => { render( , ); expect(screen.queryByRole('button', { name: /生成图片尺寸/u })).toBeNull(); fireEvent.click( screen.getByRole('button', { name: '生成图片模型 nanobanana2' }), ); fireEvent.click( within(screen.getByRole('menu', { name: '生成图片模型选项' })).getByRole( 'button', { name: 'gpt-image-2' }, ), ); expect(screen.getByLabelText('当前模型').textContent).toBe( IMAGE_MODEL_GPT_IMAGE_2, ); expect(screen.getByLabelText('当前比例').textContent).toBe('4:3'); expect(screen.getByLabelText('当前尺寸').textContent).toBe('1K'); expect(screen.getByLabelText('当前占位').textContent).toBe( '120:160:720:540:720:540', ); }); it('keeps the canvas placeholder centered while syncing its ratio and size selection', () => { render( , ); fireEvent.click( screen.getByRole('button', { name: '生成图片尺寸 1:1·1K' }), ); const panel = screen.getByRole('menu', { name: '生成图片尺寸选项' }); fireEvent.click(within(panel).getByRole('button', { name: '16:9' })); fireEvent.click(within(panel).getByRole('button', { name: '2K' })); expect(screen.getByLabelText('当前占位').textContent).toBe( '-714:-166:2048:1152:2048:1152', ); }); });