/* @vitest-environment jsdom */ import { fireEvent, render, screen, within } from '@testing-library/react'; import type { ComponentProps, ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { AuthUiContext } from '../auth/AuthUiContext'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView'; const DARK_AUTH_UI_VALUE = { platformTheme: 'dark', } as ComponentProps['value']; function withDarkAuthUi(children: ReactNode) { return ( {children} ); } function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-1', resourceId: 'resource-1', title: '生成主图', src: 'data:image/png;base64,layer', x: 0, y: 0, width: 320, height: 240, originalWidth: 1024, originalHeight: 768, zIndex: 1, sourceType: 'generated', ...overrides, }; } describe('ImageCanvasMetadataModalView', () => { it('renders generated layer metadata with generation inputs and references', () => { render( withDarkAuthUi( , ), ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(dialog.parentElement?.className).toContain('platform-theme--light'); expect(dialog.parentElement?.className).not.toContain( 'platform-theme--dark', ); expect(within(dialog).queryByText('生成主图')).toBeNull(); expect(within(dialog).getByText('生成图片')).toBeTruthy(); expect(within(dialog).getByText('生成提示词')).toBeTruthy(); expect(within(dialog).getByText('清爽游戏按钮')).toBeTruthy(); expect(within(dialog).getByText('参考图')).toBeTruthy(); expect(within(dialog).getByText('角色立绘')).toBeTruthy(); expect( within(dialog).getByText('项目资源 · resource-reference'), ).toBeTruthy(); expect(within(dialog).getByText('Model')).toBeTruthy(); expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy(); expect(within(dialog).queryByText('Provider')).toBeNull(); expect(within(dialog).queryByText(/VectorEngine/u)).toBeNull(); expect(within(dialog).queryByText(/task-/u)).toBeNull(); expect(within(dialog).getByText('123')).toBeTruthy(); }); it('renders game scene semantics for generated scene metadata', () => { render( , ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(within(dialog).getByText('游戏场景')).toBeTruthy(); expect(within(dialog).queryByText('生成图片')).toBeNull(); }); it('keeps the generation model while hiding internal processing models', () => { const generationInputsWithInternalMattingMetadata = { fields: [ { title: '处理阶段', value: '角色抠图' }, { title: '处理模型', value: 'birefnet' }, ], references: [], mattingProvider: 'Aliyun Matting', mattingModel: 'segment-common-image', }; const { rerender } = render( , ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(within(dialog).getByText('处理阶段')).toBeTruthy(); expect(within(dialog).getByText('角色抠图')).toBeTruthy(); expect(within(dialog).queryByText('处理模型')).toBeNull(); expect(within(dialog).getByText('Model')).toBeTruthy(); expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); expect(within(dialog).queryByText('birefnet')).toBeNull(); expect(within(dialog).queryByText('Aliyun Matting')).toBeNull(); expect(within(dialog).queryByText('segment-common-image')).toBeNull(); rerender( , ); expect(within(dialog).getByText('Model')).toBeTruthy(); expect(within(dialog).queryByText('birefnet')).toBeNull(); }); it('renders nanobanana2 instead of the raw Gemini image model id', () => { render( , ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(within(dialog).getByText('nanobanana2')).toBeTruthy(); expect( within(dialog).queryByText('gemini-3.1-flash-image-preview'), ).toBeNull(); }); it('renders upload fallback values and closes through the modal shell', () => { const onClose = vi.fn(); render( , ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(within(dialog).queryByText('上传素材')).toBeNull(); expect(within(dialog).getByText('上传图片')).toBeTruthy(); expect(within(dialog).getAllByText('-').length).toBeGreaterThanOrEqual(3); fireEvent.click( within(dialog).getByRole('button', { name: '关闭图片信息' }), ); expect(onClose).toHaveBeenCalledTimes(1); }); it('renders generated video metadata with video labels and close action', () => { const onClose = vi.fn(); render( , ); const dialog = screen.getByRole('dialog', { name: '视频信息' }); expect(within(dialog).getByText('视频类型')).toBeTruthy(); expect(within(dialog).getByText('生成视频')).toBeTruthy(); expect(within(dialog).getByText('视频描述')).toBeTruthy(); expect(within(dialog).getByText('让角色向镜头挥手')).toBeTruthy(); expect(within(dialog).getByText('kling3.0-omni')).toBeTruthy(); expect(within(dialog).getByText('1280 x 720 px')).toBeTruthy(); fireEvent.click( within(dialog).getByRole('button', { name: '关闭视频信息' }), ); expect(onClose).toHaveBeenCalledTimes(1); }); it('renders true and false generation input values as visible text', () => { render( , ); const dialog = screen.getByRole('dialog', { name: '图片信息' }); expect(within(dialog).getByText('true')).toBeTruthy(); expect(within(dialog).getByText('false')).toBeTruthy(); }); it('renders audio duration from generation inputs instead of layer metadata', () => { render( , ); const dialog = screen.getByRole('dialog', { name: '音频信息' }); expect(within(dialog).getByText('音频类型')).toBeTruthy(); expect(within(dialog).getByText('游戏背景音乐')).toBeTruthy(); expect(within(dialog).getByText('音乐描述')).toBeTruthy(); expect(within(dialog).getByText('紧张的地下城循环音乐')).toBeTruthy(); expect(within(dialog).getByText('时长')).toBeTruthy(); expect(within(dialog).getByText('75秒')).toBeTruthy(); expect(within(dialog).queryByText('Resolution')).toBeNull(); expect(within(dialog).queryByText('420 x 120 px')).toBeNull(); }); it('renders SFX V2 authoritative prompts, actual duration, loop, model, and full task id', () => { render( , ); const dialog = screen.getByRole('dialog', { name: '音频信息' }); for (const text of [ '用户描述', '金币落地', '实际英文提示词', 'A bright coin landing chime', '7.42秒', 'Loop', '开启', 'eleven_text_to_sound_v2', 'task-sfx-v2-1234-abcd', ]) { expect(within(dialog).getByText(text)).toBeTruthy(); } }); it('keeps the full sound effect task id when SFX metadata is unavailable', () => { render( , ); // 中文注释:音效 taskId 是 operation id 形态,「取最后一段数字」会截出 1234 这种误导值。 // Task 这一栏不能因为 soundEffect 元数据缺失就降级成假值。 const dialog = screen.getByRole('dialog', { name: '音频信息' }); expect(within(dialog).getByText('task-sfx-v2-1234-abcd')).toBeTruthy(); expect(within(dialog).queryByText('1234')).toBeNull(); }); it('keeps the task id display tied to the generating model across asset tag overrides', () => { // 中文注释:展示口径由「谁生成了这个 taskId」决定,也就是 model。素材标签只改 // assetKindOverride,不该影响它:背景音乐被改标成「音效」不能换用完整 id,音效被改标成 // 「背景音乐」更不能退回截断假值。 const { unmount } = render( , ); const musicDialog = screen.getByRole('dialog', { name: '音频信息' }); expect(within(musicDialog).getByText('75')).toBeTruthy(); expect(within(musicDialog).queryByText('audio-task-75')).toBeNull(); unmount(); render( , ); const soundEffectDialog = screen.getByRole('dialog', { name: '音频信息' }); expect( within(soundEffectDialog).getByText('task-sfx-v2-1234-abcd'), ).toBeTruthy(); expect(within(soundEffectDialog).queryByText('1234')).toBeNull(); }); it('keeps the full sound effect task id when the resource kind is unresolvable', () => { // 中文注释:legacy 布局快照没有 assetKindOverride 键,水合时旧的 assetKind 会被当成 // override;资源行又缺 assetKind(或整个 resource 查不到)时 resourceAssetKind 是 null。 // 这正是元数据缺失最常伴随的形态,判据不能依赖资源分类,否则又退回截断假值。 render( , ); const dialog = screen.getByRole('dialog', { name: '音频信息' }); expect(within(dialog).getByText('task-sfx-v2-1234-abcd')).toBeTruthy(); expect(within(dialog).queryByText('1234')).toBeNull(); }); it('does not render a dialog when no layer is selected', () => { render(); expect(screen.queryByRole('dialog', { name: '图片信息' })).toBeNull(); }); });