Files
Genarrative/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx
T
JenkenB c836ac604d Treat character-action generations as video & UI tweaks
Handle character-action generation results as real video assets: use backend previewVideoPath as layer src, set mediaType="video" and assetKind="character-animation", and store the first frame only as thumbnail/poster. Update docs to document the new behavior and preserve backend frame extraction/green-screen workflows. Update UI/UX: compact parameter labels (e.g. "16:9·1K"), quick-edit treats original image as an implicit last reference (hidden from the reference bar) and allows editable bottom capsules, and ensure exports preserve true media file types. Change upload toolbar icon to Upload and add tests to cover the icon and video-layer behavior; also add serialization/hydration test for video thumbnails and adjust many integration/unit tests to reflect new labels, sizes and generation semantics. Files touched include multiple docs under docs/, edited components and tests under src/components/image-editor/, and minor stylesheet updates.
2026-06-22 13:45:20 +08:00

194 lines
6.7 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView';
function createLayer(overrides: Partial<CanvasLayer> = {}): 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(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'gpt-image-2-task-123',
objectKey: 'generated/object.png',
generationInputs: {
fields: [{ title: '生成提示词', value: '清爽游戏按钮' }],
references: [
{
title: '参考图',
label: '角色立绘',
refType: 'project-resource',
refId: 'resource-reference',
},
],
},
})}
onClose={vi.fn()}
/>,
);
const dialog = screen.getByRole('dialog', { name: '图片信息' });
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('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();
expect(within(dialog).getByText('generated/object.png')).toBeTruthy();
});
it('renders nanobanana2 instead of the raw Gemini image model id', () => {
render(
<ImageCanvasMetadataModalView
layer={createLayer({
model: 'gemini-3.1-flash-image-preview',
})}
onClose={vi.fn()}
/>,
);
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(
<ImageCanvasMetadataModalView
layer={createLayer({
title: '上传素材',
sourceType: 'uploaded',
model: null,
provider: null,
taskId: null,
objectKey: null,
assetObjectId: 'asset-object-1',
generationInputs: null,
})}
onClose={onClose}
/>,
);
const dialog = screen.getByRole('dialog', { name: '图片信息' });
expect(within(dialog).queryByText('上传素材')).toBeNull();
expect(within(dialog).getByText('上传图片')).toBeTruthy();
expect(within(dialog).getAllByText('-').length).toBeGreaterThanOrEqual(3);
expect(within(dialog).getByText('asset-object-1')).toBeTruthy();
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(
<ImageCanvasMetadataModalView
layer={createLayer({
title: '生成视频 1',
src: '/generated-editor-videos/video-task-1/preview.mp4',
mediaType: 'video',
assetKind: 'video',
originalWidth: 1280,
originalHeight: 720,
model: 'kling3.0-omni',
taskId: 'video-task-1',
generationInputs: {
fields: [{ title: '视频描述', value: '让角色向镜头挥手' }],
references: [],
},
})}
onClose={onClose}
/>,
);
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 generated audio metadata with duration instead of resolution', () => {
render(
<ImageCanvasMetadataModalView
layer={createLayer({
title: '游戏背景音乐',
src: '/generated-character-drafts/editor-audios/music.wav',
mediaType: 'audio',
assetKind: 'background-music',
originalWidth: 420,
originalHeight: 120,
model: 'suno',
taskId: 'audio-task-75',
durationSeconds: 75,
generationInputs: {
fields: [{ title: '音乐描述', value: '紧张的地下城循环音乐' }],
references: [],
},
})}
onClose={vi.fn()}
/>,
);
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('1:15')).toBeTruthy();
expect(within(dialog).queryByText('Resolution')).toBeNull();
expect(within(dialog).queryByText('420 x 120 px')).toBeNull();
});
it('does not render a dialog when no layer is selected', () => {
render(<ImageCanvasMetadataModalView layer={null} onClose={vi.fn()} />);
expect(screen.queryByRole('dialog', { name: '图片信息' })).toBeNull();
});
});