ea29887d9c
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交 - 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示 - 快速编辑提交前同步改写原图引用为实际图序号 - 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象 - 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
678 lines
22 KiB
TypeScript
678 lines
22 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasWorldView } from './ImageCanvasWorldView';
|
|
|
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-1',
|
|
resourceId: 'resource-1',
|
|
title: '角色主图',
|
|
src: 'data:image/png;base64,layer',
|
|
x: 120,
|
|
y: 160,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 640,
|
|
originalHeight: 480,
|
|
zIndex: 1,
|
|
sourceType: 'uploaded',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createGenerationDialog(
|
|
overrides: Partial<CanvasGenerationDialogState> = {},
|
|
): CanvasGenerationDialogState {
|
|
return {
|
|
id: 'dialog-1',
|
|
mode: 'generate',
|
|
prompt: '生成一张图',
|
|
status: 'idle',
|
|
placeholder: {
|
|
x: 480,
|
|
y: 320,
|
|
width: 360,
|
|
height: 240,
|
|
originalWidth: 1024,
|
|
originalHeight: 768,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function renderWorldView(
|
|
overrides: Partial<Parameters<typeof ImageCanvasWorldView>[0]> = {},
|
|
) {
|
|
const props: Parameters<typeof ImageCanvasWorldView>[0] = {
|
|
viewport: { x: 10, y: 20, scale: 1.5 },
|
|
snapGuide: null,
|
|
layers: [createLayer()],
|
|
selectedLayerIds: [],
|
|
hoveredLayerId: null,
|
|
canvasMarquee: null,
|
|
canvasGenerationDialogs: [],
|
|
generateDialog: null,
|
|
cropExpandPanel: null,
|
|
cropExpandSourceLayer: null,
|
|
generationComposerStyle: null,
|
|
onLayerPointerDown: vi.fn(),
|
|
onLayerClick: vi.fn(),
|
|
onLayerContextMenu: vi.fn(),
|
|
onLayerMouseEnter: vi.fn(),
|
|
onLayerMouseLeave: vi.fn(),
|
|
onOpenLayerMetadata: vi.fn(),
|
|
onGenerationFramePointerDown: vi.fn(),
|
|
onActivateGenerationDialog: vi.fn(),
|
|
onCropExpandHandlePointerDown: vi.fn(),
|
|
...overrides,
|
|
};
|
|
|
|
const result = render(<ImageCanvasWorldView {...props} />);
|
|
|
|
return { props, ...result };
|
|
}
|
|
|
|
describe('ImageCanvasWorldView', () => {
|
|
it('renders visible layers and filters hidden layers', () => {
|
|
const visibleLayer = createLayer({ title: '可见图层', zIndex: 2 });
|
|
const hiddenLayer = createLayer({
|
|
id: 'layer-hidden',
|
|
resourceId: 'resource-hidden',
|
|
title: '隐藏图层',
|
|
hidden: true,
|
|
});
|
|
|
|
renderWorldView({
|
|
layers: [hiddenLayer, visibleLayer],
|
|
selectedLayerIds: [visibleLayer.id],
|
|
hoveredLayerId: visibleLayer.id,
|
|
});
|
|
|
|
const visibleButton = screen.getByRole('button', { name: '选择可见图层' });
|
|
|
|
expect(screen.queryByRole('button', { name: '选择隐藏图层' })).toBeNull();
|
|
expect(visibleButton.className).toContain(
|
|
'image-canvas-editor__layer--selected',
|
|
);
|
|
expect(visibleButton.className).toContain(
|
|
'image-canvas-editor__layer--hovered',
|
|
);
|
|
expect(visibleButton.className).not.toContain(
|
|
'image-canvas-editor__layer--generating',
|
|
);
|
|
expect(visibleButton.style.left).toBe('120px');
|
|
expect(visibleButton.style.top).toBe('160px');
|
|
expect(screen.getByText('640 x 480 px')).toBeTruthy();
|
|
expect(screen.queryByRole('status', { name: '' })).toBeNull();
|
|
});
|
|
|
|
it('forwards layer pointer, hover, context menu and metadata actions', () => {
|
|
const layer = createLayer({ assetKind: 'character' });
|
|
const { props } = renderWorldView({ layers: [layer] });
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择角色主图' });
|
|
const metadataButton = within(layerButton).getByRole('button', {
|
|
name: '查看角色主图图片信息',
|
|
});
|
|
|
|
fireEvent.pointerDown(layerButton);
|
|
fireEvent.click(layerButton);
|
|
fireEvent.contextMenu(layerButton);
|
|
fireEvent.mouseEnter(layerButton);
|
|
fireEvent.mouseLeave(layerButton);
|
|
fireEvent.click(metadataButton);
|
|
|
|
expect(props.onLayerPointerDown).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
layer,
|
|
);
|
|
expect(props.onLayerClick).toHaveBeenCalledWith(expect.any(Object), layer);
|
|
expect(props.onLayerContextMenu).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
layer,
|
|
);
|
|
expect(props.onLayerMouseEnter).toHaveBeenCalledWith(layer.id);
|
|
expect(props.onLayerMouseLeave).toHaveBeenCalledWith(layer.id);
|
|
expect(props.onOpenLayerMetadata).toHaveBeenCalledWith(layer);
|
|
expect(screen.getByText('角色')).toBeTruthy();
|
|
});
|
|
|
|
it('renders snap guides, marquee and floating generation status', () => {
|
|
renderWorldView({
|
|
snapGuide: { vertical: 288, horizontal: 344 },
|
|
canvasMarquee: {
|
|
pointerId: 1,
|
|
startX: 40,
|
|
startY: 50,
|
|
currentX: 190,
|
|
currentY: 230,
|
|
},
|
|
generateDialog: {
|
|
id: 'dialog-active',
|
|
mode: 'generate',
|
|
prompt: '生成中',
|
|
status: 'generating',
|
|
},
|
|
generationComposerStyle: { left: 12, top: 24 },
|
|
});
|
|
|
|
expect(
|
|
screen.getByTestId('image-canvas-editor-snap-guide-vertical').style.left,
|
|
).toBe('288px');
|
|
expect(
|
|
screen.getByTestId('image-canvas-editor-snap-guide-horizontal').style.top,
|
|
).toBe('344px');
|
|
expect(screen.getByText('生成中')).toBeTruthy();
|
|
|
|
const world = document.querySelector('.image-canvas-editor__world');
|
|
const marquee = world?.querySelector('.image-canvas-editor__canvas-marquee');
|
|
|
|
expect(world?.getAttribute('style')).toContain(
|
|
'transform: translate(10px, 20px) scale(1.5)',
|
|
);
|
|
expect((marquee as HTMLElement | null)?.style.width).toBe('100px');
|
|
expect((marquee as HTMLElement | null)?.style.height).toBe('120px');
|
|
});
|
|
|
|
it('renders generation placeholders and forwards frame actions', () => {
|
|
const dialog = createGenerationDialog({
|
|
mode: 'icon',
|
|
status: 'generating',
|
|
});
|
|
const { props } = renderWorldView({
|
|
canvasGenerationDialogs: [
|
|
dialog,
|
|
createGenerationDialog({
|
|
id: 'dialog-without-placeholder',
|
|
placeholder: undefined,
|
|
}),
|
|
],
|
|
generateDialog: dialog,
|
|
});
|
|
|
|
const frame = screen.getByRole('button', { name: '图标素材生成占位图' });
|
|
|
|
expect(frame.className).toContain(
|
|
'image-canvas-editor__generation-frame--focused',
|
|
);
|
|
expect(within(frame).getByText('Icon Generator')).toBeTruthy();
|
|
expect(within(frame).getByText('图标')).toBeTruthy();
|
|
expect(within(frame).getByText('1024 x 768')).toBeTruthy();
|
|
expect(within(frame).getByRole('status').textContent).toBe('生成中');
|
|
|
|
fireEvent.pointerDown(frame);
|
|
fireEvent.doubleClick(frame);
|
|
|
|
expect(props.onGenerationFramePointerDown).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
dialog,
|
|
);
|
|
expect(props.onActivateGenerationDialog).toHaveBeenCalledWith(dialog);
|
|
expect(screen.queryByText('dialog-without-placeholder')).toBeNull();
|
|
});
|
|
|
|
it('renders crop-expand frame handles and forwards drag actions', () => {
|
|
const layer = createLayer();
|
|
const { props } = renderWorldView({
|
|
layers: [layer],
|
|
cropExpandSourceLayer: layer,
|
|
cropExpandPanel: {
|
|
sourceLayerId: layer.id,
|
|
frame: { x: 100, y: 140, width: 360, height: 300 },
|
|
ratio: 'free',
|
|
status: 'idle',
|
|
},
|
|
});
|
|
|
|
const frame = screen.getByRole('group', { name: '裁扩边界' });
|
|
const rightHandle = within(frame).getByRole('button', {
|
|
name: '拖拽裁扩右边',
|
|
});
|
|
|
|
expect(frame.style.left).toBe('100px');
|
|
expect(frame.style.width).toBe('360px');
|
|
|
|
fireEvent.pointerDown(rightHandle);
|
|
|
|
expect(props.onCropExpandHandlePointerDown).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
'right',
|
|
);
|
|
});
|
|
|
|
it('hides idle placeholder chrome after blur and restores it when focused', () => {
|
|
const dialog = createGenerationDialog({
|
|
id: 'dialog-idle',
|
|
mode: 'generate',
|
|
status: 'idle',
|
|
});
|
|
|
|
const { rerender, props } = renderWorldView({
|
|
canvasGenerationDialogs: [dialog],
|
|
generateDialog: null,
|
|
});
|
|
|
|
const blurredFrame = screen.getByRole('button', { name: '图像生成占位图' });
|
|
expect(blurredFrame.className).not.toContain(
|
|
'image-canvas-editor__generation-frame--focused',
|
|
);
|
|
expect(within(blurredFrame).queryByText('Image Generator')).toBeNull();
|
|
expect(within(blurredFrame).queryByText('1024 x 768')).toBeNull();
|
|
|
|
rerender(<ImageCanvasWorldView {...props} generateDialog={dialog} />);
|
|
|
|
const focusedFrame = screen.getByRole('button', { name: '图像生成占位图' });
|
|
expect(focusedFrame.className).toContain(
|
|
'image-canvas-editor__generation-frame--focused',
|
|
);
|
|
expect(within(focusedFrame).getByText('Image Generator')).toBeTruthy();
|
|
expect(within(focusedFrame).getByText('1024 x 768')).toBeTruthy();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
mode: 'character',
|
|
ariaLabel: '角色生成占位图',
|
|
generatorLabel: 'Character Generator',
|
|
badgeLabel: '角色',
|
|
frameClassName: 'image-canvas-editor__generation-frame--character',
|
|
iconClassName: 'lucide-user-round',
|
|
},
|
|
{
|
|
mode: 'character-animation',
|
|
ariaLabel: '角色动作生成占位图',
|
|
generatorLabel: 'Action Generator',
|
|
badgeLabel: '动作',
|
|
frameClassName:
|
|
'image-canvas-editor__generation-frame--character-animation',
|
|
iconClassName: 'lucide-person-standing',
|
|
},
|
|
{
|
|
mode: 'video',
|
|
ariaLabel: '视频生成占位图',
|
|
generatorLabel: 'Video Generator',
|
|
badgeLabel: '视频',
|
|
frameClassName: 'image-canvas-editor__generation-frame--video',
|
|
iconClassName: 'lucide-film',
|
|
},
|
|
{
|
|
mode: 'audio-sound-effect',
|
|
ariaLabel: '音效生成占位图',
|
|
generatorLabel: 'Sound Generator',
|
|
badgeLabel: '音效',
|
|
frameClassName:
|
|
'image-canvas-editor__generation-frame--audio-sound-effect',
|
|
iconClassName: 'lucide-volume-2',
|
|
},
|
|
{
|
|
mode: 'audio-background-music',
|
|
ariaLabel: '背景音乐生成占位图',
|
|
generatorLabel: 'Music Generator',
|
|
badgeLabel: '背景音乐',
|
|
frameClassName:
|
|
'image-canvas-editor__generation-frame--audio-background-music',
|
|
iconClassName: 'lucide-music-2',
|
|
},
|
|
])(
|
|
'renders $mode generation placeholder with its own icon and badge',
|
|
({ mode, ariaLabel, generatorLabel, badgeLabel, frameClassName, iconClassName }) => {
|
|
const dialog = createGenerationDialog({
|
|
id: `dialog-${mode}`,
|
|
mode: mode as CanvasGenerationDialogState['mode'],
|
|
});
|
|
|
|
renderWorldView({
|
|
canvasGenerationDialogs: [dialog],
|
|
generateDialog: dialog as GenerateDialogState,
|
|
});
|
|
|
|
const frame = screen.getByRole('button', { name: ariaLabel });
|
|
const generatorLabelNode = within(frame).getByText(
|
|
generatorLabel,
|
|
) as HTMLElement;
|
|
expect(frame.className).toContain(frameClassName);
|
|
expect(generatorLabelNode).toBeTruthy();
|
|
expect(
|
|
generatorLabelNode.querySelector('svg')?.getAttribute('class'),
|
|
).toContain(iconClassName);
|
|
expect(within(frame).getByText(badgeLabel)).toBeTruthy();
|
|
expect(
|
|
frame
|
|
.querySelector('.image-canvas-editor__generation-frame-icon svg')
|
|
?.getAttribute('class'),
|
|
).toContain(iconClassName);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
mode: 'character',
|
|
ariaLabel: '角色生成占位图',
|
|
generatorLabel: 'Character Generator',
|
|
badgeLabel: '角色',
|
|
},
|
|
{
|
|
mode: 'character-animation',
|
|
ariaLabel: '角色动作生成占位图',
|
|
generatorLabel: 'Action Generator',
|
|
badgeLabel: '动作',
|
|
},
|
|
{
|
|
mode: 'video',
|
|
ariaLabel: '视频生成占位图',
|
|
generatorLabel: 'Video Generator',
|
|
badgeLabel: '视频',
|
|
},
|
|
{
|
|
mode: 'audio-sound-effect',
|
|
ariaLabel: '音效生成占位图',
|
|
generatorLabel: 'Sound Generator',
|
|
badgeLabel: '音效',
|
|
},
|
|
{
|
|
mode: 'audio-background-music',
|
|
ariaLabel: '背景音乐生成占位图',
|
|
generatorLabel: 'Music Generator',
|
|
badgeLabel: '背景音乐',
|
|
},
|
|
])(
|
|
'keeps $mode generation title size and badge readable when zoomed out',
|
|
({ mode, ariaLabel, generatorLabel, badgeLabel }) => {
|
|
const dialog = createGenerationDialog({
|
|
id: `dialog-zoom-${mode}`,
|
|
mode: mode as CanvasGenerationDialogState['mode'],
|
|
});
|
|
|
|
renderWorldView({
|
|
viewport: { x: 0, y: 0, scale: 0.25 },
|
|
canvasGenerationDialogs: [dialog],
|
|
generateDialog: dialog as GenerateDialogState,
|
|
});
|
|
|
|
const frame = screen.getByRole('button', { name: ariaLabel });
|
|
const inverseScale = '4';
|
|
|
|
expect(
|
|
(within(frame).getByText(generatorLabel) as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
(within(frame).getByText('1024 x 768') as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
(within(frame).getByText(badgeLabel) as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
mode: 'character',
|
|
ariaLabel: '角色生成占位图',
|
|
generatorLabel: 'Character Generator',
|
|
badgeLabel: '角色',
|
|
},
|
|
{
|
|
mode: 'character-animation',
|
|
ariaLabel: '角色动作生成占位图',
|
|
generatorLabel: 'Action Generator',
|
|
badgeLabel: '动作',
|
|
},
|
|
{
|
|
mode: 'video',
|
|
ariaLabel: '视频生成占位图',
|
|
generatorLabel: 'Video Generator',
|
|
badgeLabel: '视频',
|
|
},
|
|
{
|
|
mode: 'audio-sound-effect',
|
|
ariaLabel: '音效生成占位图',
|
|
generatorLabel: 'Sound Generator',
|
|
badgeLabel: '音效',
|
|
},
|
|
{
|
|
mode: 'audio-background-music',
|
|
ariaLabel: '背景音乐生成占位图',
|
|
generatorLabel: 'Music Generator',
|
|
badgeLabel: '背景音乐',
|
|
},
|
|
])(
|
|
'keeps $mode placeholder icon and badge visible after blur without helper chrome',
|
|
({ mode, ariaLabel, generatorLabel, badgeLabel }) => {
|
|
const dialog = createGenerationDialog({
|
|
id: `dialog-blurred-${mode}`,
|
|
mode: mode as CanvasGenerationDialogState['mode'],
|
|
status: 'idle',
|
|
});
|
|
|
|
renderWorldView({
|
|
generateDialog: null,
|
|
canvasGenerationDialogs: [dialog],
|
|
});
|
|
|
|
const frame = screen.getByRole('button', { name: ariaLabel });
|
|
expect(frame.className).not.toContain(
|
|
'image-canvas-editor__generation-frame--focused',
|
|
);
|
|
expect(within(frame).queryByText(generatorLabel)).toBeNull();
|
|
expect(within(frame).queryByText('1024 x 768')).toBeNull();
|
|
expect(within(frame).getByText(badgeLabel)).toBeTruthy();
|
|
expect(
|
|
frame.querySelector('.image-canvas-editor__generation-frame-icon svg'),
|
|
).toBeTruthy();
|
|
},
|
|
);
|
|
|
|
it('keeps layer and generation chrome readable when the canvas is zoomed out', () => {
|
|
const layer = createLayer({ assetKind: 'character' });
|
|
const dialog = createGenerationDialog({ id: 'dialog-focused' });
|
|
|
|
renderWorldView({
|
|
viewport: { x: 0, y: 0, scale: 0.25 },
|
|
layers: [layer],
|
|
hoveredLayerId: layer.id,
|
|
canvasGenerationDialogs: [dialog],
|
|
generateDialog: dialog,
|
|
});
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择角色主图' });
|
|
const frame = screen.getByRole('button', { name: '图像生成占位图' });
|
|
const inverseScale = '4';
|
|
|
|
expect(
|
|
(within(layerButton).getByText('角色') as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
(within(layerButton).getByText('640 x 480 px') as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
within(layerButton)
|
|
.getByRole('button', { name: '查看角色主图图片信息' })
|
|
.style.getPropertyValue('--image-canvas-editor-inverse-scale'),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
(within(frame).getByText('Image Generator') as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
expect(
|
|
(within(frame).getByText('1024 x 768') as HTMLElement).style.getPropertyValue(
|
|
'--image-canvas-editor-inverse-scale',
|
|
),
|
|
).toBe(inverseScale);
|
|
});
|
|
|
|
it('uses a plain information icon for metadata corner actions', () => {
|
|
const layer = createLayer();
|
|
const { props } = renderWorldView({ layers: [layer] });
|
|
|
|
const metadataButton = screen.getByRole('button', {
|
|
name: '查看角色主图图片信息',
|
|
});
|
|
|
|
expect(metadataButton.querySelector('svg')?.getAttribute('class')).toContain(
|
|
'lucide-info',
|
|
);
|
|
});
|
|
|
|
it('renders audio layers with an audio control card instead of an image', () => {
|
|
const layer = createLayer({
|
|
title: '游戏音效',
|
|
src: '/generated-character-drafts/editor-audios/sfx.mp3',
|
|
mediaType: 'audio',
|
|
assetKind: 'sound-effect',
|
|
width: 420,
|
|
height: 120,
|
|
originalWidth: 420,
|
|
originalHeight: 120,
|
|
});
|
|
|
|
const { props } = renderWorldView({ layers: [layer] });
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择游戏音效' });
|
|
const audio = within(layerButton).getByLabelText('画布音频:游戏音效');
|
|
expect(audio.tagName.toLowerCase()).toBe('audio');
|
|
expect(within(layerButton).queryByAltText('画布图片:游戏音效')).toBeNull();
|
|
expect(within(layerButton).getByText('音效')).toBeTruthy();
|
|
expect(within(layerButton).getByText('时长 --:--')).toBeTruthy();
|
|
});
|
|
|
|
it('keeps audio controls interactive and shows the generated prompt', () => {
|
|
const layer = createLayer({
|
|
title: '游戏背景音乐',
|
|
src: 'data:audio/mpeg;base64,audio',
|
|
mediaType: 'audio',
|
|
assetKind: 'background-music',
|
|
width: 420,
|
|
height: 120,
|
|
originalWidth: 420,
|
|
originalHeight: 120,
|
|
prompt: '紧张的地下城循环音乐',
|
|
durationSeconds: 75,
|
|
});
|
|
const { props } = renderWorldView({
|
|
layers: [layer],
|
|
selectedLayerIds: [layer.id],
|
|
});
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择游戏背景音乐' });
|
|
const audio = within(layerButton).getByLabelText('画布音频:游戏背景音乐');
|
|
|
|
expect(within(layerButton).getByText('紧张的地下城循环音乐')).toBeTruthy();
|
|
expect(within(layerButton).getByText('时长 1:15')).toBeTruthy();
|
|
|
|
fireEvent.pointerDown(audio);
|
|
fireEvent.click(audio);
|
|
|
|
expect(props.onLayerPointerDown).not.toHaveBeenCalled();
|
|
expect(props.onLayerClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('places layer type labels to the left of the information action', () => {
|
|
const layer = createLayer({ assetKind: 'character' });
|
|
const { props } = renderWorldView({ layers: [layer] });
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择角色主图' });
|
|
const badge = within(layerButton).getByText('角色');
|
|
const metadataButton = within(layerButton).getByRole('button', {
|
|
name: '查看角色主图图片信息',
|
|
});
|
|
|
|
expect(badge.className).toContain(
|
|
'image-canvas-editor__kind-badge--beside-info',
|
|
);
|
|
expect(metadataButton.className).not.toContain(
|
|
'image-canvas-editor__metadata-corner--beside-kind',
|
|
);
|
|
});
|
|
|
|
it('shows duration instead of resolution for focused audio generation frames', () => {
|
|
const dialog = createGenerationDialog({
|
|
id: 'dialog-audio',
|
|
mode: 'audio-sound-effect',
|
|
});
|
|
|
|
renderWorldView({
|
|
canvasGenerationDialogs: [dialog],
|
|
generateDialog: dialog,
|
|
});
|
|
|
|
const frame = screen.getByRole('button', { name: '音效生成占位图' });
|
|
expect(within(frame).getByText('时长 --:--')).toBeTruthy();
|
|
expect(within(frame).queryByText('1024 x 768')).toBeNull();
|
|
});
|
|
|
|
it('renders video layers with a playable video element instead of an image', () => {
|
|
const layer = createLayer({
|
|
title: '生成视频 7',
|
|
src: '/generated-character-drafts/editor-videos/video-7.mp4',
|
|
mediaType: 'video',
|
|
assetKind: 'video',
|
|
width: 854,
|
|
height: 480,
|
|
originalWidth: 854,
|
|
originalHeight: 480,
|
|
});
|
|
|
|
const { props } = renderWorldView({ layers: [layer] });
|
|
|
|
const layerButton = screen.getByRole('button', { name: '选择生成视频 7' });
|
|
const video = within(layerButton).getByLabelText('画布视频:生成视频 7');
|
|
fireEvent.pointerDown(video);
|
|
fireEvent.click(video);
|
|
|
|
expect(video.tagName.toLowerCase()).toBe('video');
|
|
expect(video.getAttribute('src')).toBe(
|
|
'/generated-character-drafts/editor-videos/video-7.mp4',
|
|
);
|
|
expect(video.hasAttribute('controls')).toBe(true);
|
|
expect(video.hasAttribute('playsinline')).toBe(true);
|
|
expect(video.getAttribute('preload')).toBe('metadata');
|
|
expect(within(layerButton).queryByAltText('画布视频:生成视频 7')).toBeNull();
|
|
expect(within(layerButton).getByText('视频')).toBeTruthy();
|
|
expect(props.onLayerPointerDown).not.toHaveBeenCalled();
|
|
expect(props.onLayerClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps saved generator placeholders hidden after a result layer exists', () => {
|
|
renderWorldView({
|
|
layers: [createLayer({ id: 'layer-generated', title: '生成图片' })],
|
|
canvasGenerationDialogs: [
|
|
createGenerationDialog({
|
|
generatedLayerId: 'layer-generated',
|
|
placeholder: {
|
|
x: 80,
|
|
y: 90,
|
|
width: 420,
|
|
height: 420,
|
|
originalWidth: 2048,
|
|
originalHeight: 2048,
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(screen.getByRole('button', { name: '选择生成图片' })).toBeTruthy();
|
|
expect(
|
|
screen.queryByRole('button', { name: '图像生成占位图' }),
|
|
).toBeNull();
|
|
});
|
|
});
|