Files
Genarrative/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
T
k88936 d901945c77
Project CI / Repository checks (push) Successful in 1m20s
Project CI / Frontend tests (push) Successful in 2m53s
Project CI / Backend tests (push) Successful in 3m56s
Project CI / Native shell tests (push) Successful in 14m1s
移除视频快速编辑支持 (#157)
当前实现: 视频快速编辑错误地复用了图片快速编辑的panel, 然后发起一个实际是生成视频的请求

策划建议删除.

因为后端没有实际意义上的视频快速编辑, 所以api 后端代码没有改动, 只在前端删除入口和处理

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/157
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-14 18:52:56 +08:00

434 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @vitest-environment jsdom */
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { ImageCanvasContextMenusView } from './ImageCanvasContextMenusView';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
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: 320,
originalHeight: 240,
zIndex: 1,
sourceType: 'uploaded',
...overrides,
};
}
function renderContextMenus(
overrides: Partial<Parameters<typeof ImageCanvasContextMenusView>[0]> = {},
) {
const props: Parameters<typeof ImageCanvasContextMenusView>[0] = {
viewport: { x: 0, y: 0, scale: 1 },
contextMenu: null,
canvasClipboard: null,
imageContextMenu: null,
imageContextMenuLayer: null,
contextMenuLayer: null,
contextShouldShowLayer: false,
contextShouldUnlockLayer: false,
canCopyContextLayers: true,
isCanvasClipboardCopyBlocked: false,
onPasteCanvasClipboard: vi.fn(),
onCopyContextLayers: vi.fn(),
onDuplicateContextLayers: vi.fn(),
onMoveContextLayers: vi.fn(),
onGroupContextLayers: vi.fn(),
onUngroupContextLayers: vi.fn(),
onToggleContextLayerVisibility: vi.fn(),
onToggleContextLayerLock: vi.fn(),
onFlipContextLayers: vi.fn(),
onExportContextLayer: vi.fn(),
onDeleteContextLayers: vi.fn(),
onDeleteLayerById: vi.fn(),
onCloseContextMenu: vi.fn(),
onCloseImageContextMenu: vi.fn(),
onUpdateScaleFromCenter: vi.fn(),
onFitLayers: vi.fn(),
onOpenQuickEditPanel: vi.fn(),
onOpenLayerMetadata: vi.fn(),
onOpenCharacterAnimationPanel: vi.fn(),
...overrides,
};
render(<ImageCanvasContextMenusView {...props} />);
return props;
}
function createDomRect(
width: number,
height: number,
overrides: Partial<DOMRect> = {},
): DOMRect {
const left = overrides.left ?? 0;
const top = overrides.top ?? 0;
return {
x: 0,
y: 0,
width,
height,
top,
left,
right: left + width,
bottom: top + height,
toJSON: () => ({}),
...overrides,
} as DOMRect;
}
describe('ImageCanvasContextMenusView', () => {
it('renders blank canvas commands and forwards the canvas point', () => {
const canvasPoint = { x: 18, y: 24 };
const layer = createLayer();
const props = renderContextMenus({
contextMenu: { kind: 'blank', x: 10, y: 12, canvasPoint },
canvasClipboard: { layers: [layer], mode: 'copy' },
});
fireEvent.click(screen.getByRole('menuitem', { name: '粘贴' }));
fireEvent.click(screen.getByRole('menuitem', { name: '放大' }));
fireEvent.click(screen.getByRole('menuitem', { name: '显示画布所有元素' }));
expect(props.onPasteCanvasClipboard).toHaveBeenCalledWith(canvasPoint);
expect(props.onUpdateScaleFromCenter).toHaveBeenCalledWith(1.16);
expect(props.onFitLayers).toHaveBeenCalledTimes(1);
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(2);
});
it('closes canvas context menus when another operation starts outside', () => {
const props = renderContextMenus({
contextMenu: {
kind: 'blank',
x: 10,
y: 12,
canvasPoint: { x: 18, y: 24 },
},
});
fireEvent.pointerDown(document.body);
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(1);
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
});
it('renders layer context commands and forwards layer operations', () => {
const layer = createLayer({
assetKind: 'character',
mediaType: 'image-sequence',
});
const props = renderContextMenus({
contextMenu: {
kind: 'layer',
x: 16,
y: 18,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
canvasClipboard: { layers: [layer], mode: 'copy' },
imageContextMenuLayer: layer,
contextMenuLayer: layer,
contextShouldShowLayer: true,
contextShouldUnlockLayer: true,
});
fireEvent.click(screen.getByRole('menuitem', { name: '复制' }));
fireEvent.click(screen.getByRole('menuitem', { name: '剪切' }));
fireEvent.click(screen.getByRole('menuitem', { name: '创建副本' }));
fireEvent.click(screen.getByRole('menuitem', { name: '上移一层' }));
fireEvent.click(screen.getByRole('menuitem', { name: '显示' }));
fireEvent.click(screen.getByRole('menuitem', { name: '解锁' }));
fireEvent.click(screen.getByRole('menuitem', { name: '水平翻转' }));
expect(
screen
.getByRole('menuitem', { name: '导出为' })
.getAttribute('aria-haspopup'),
).toBe('menu');
fireEvent.click(
screen.getByRole('menuitem', { name: '序列帧导出(zip' }),
);
fireEvent.click(
screen.getByRole('menuitem', { name: 'Spine 导出(zip' }),
);
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
fireEvent.click(screen.getByRole('menuitem', { name: '生成动画' }));
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
expect(props.onCopyContextLayers).toHaveBeenNthCalledWith(1);
expect(props.onCopyContextLayers).toHaveBeenNthCalledWith(2, { cut: true });
expect(props.onDuplicateContextLayers).toHaveBeenCalledTimes(1);
expect(props.onMoveContextLayers).toHaveBeenCalledWith('up');
expect(props.onToggleContextLayerVisibility).toHaveBeenCalledTimes(1);
expect(props.onToggleContextLayerLock).toHaveBeenCalledTimes(1);
expect(props.onFlipContextLayers).toHaveBeenCalledWith('x');
expect(props.onExportContextLayer).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ mode: 'sequence-with-preview' }),
);
expect(props.onExportContextLayer).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ mode: 'spine-json' }),
);
expect(props.onOpenQuickEditPanel).not.toHaveBeenCalled();
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(1);
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
expect(props.onDeleteContextLayers).toHaveBeenCalledTimes(1);
});
it('disables copy actions while the target resource is still being saved', () => {
const layer = createLayer({ resourceId: 'local-resource-pending' });
const props = renderContextMenus({
contextMenu: {
kind: 'layer',
x: 16,
y: 18,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
contextMenuLayer: layer,
canCopyContextLayers: false,
contextLayerCopyBlockReason: 'pending-resource',
});
expect(
(screen.getByRole('menuitem', { name: '复制' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('menuitem', { name: '剪切' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('menuitem', { name: '创建副本' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(props.onCopyContextLayers).not.toHaveBeenCalled();
expect(props.onDuplicateContextLayers).not.toHaveBeenCalled();
expect(
screen.getByRole('menuitem', { name: '复制' }).getAttribute('title'),
).toBe('素材仍在保存,请稍后重试');
});
it('explains that persisted historical sequences cannot be copied', () => {
const layer = createLayer({
resourceId: 'local-resource-historical-sequence',
resourcePersistenceState: 'self-contained-local',
mediaType: 'image-sequence',
});
renderContextMenus({
contextMenu: {
kind: 'layer',
x: 16,
y: 18,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
contextMenuLayer: layer,
canCopyContextLayers: false,
contextLayerCopyBlockReason: 'self-contained-local-unsupported',
});
expect(
screen.getByRole('menuitem', { name: '复制' }).getAttribute('title'),
).toBe('历史序列帧素材暂不支持复制');
});
it('explains that unresolved local resources are not registered', () => {
const layer = createLayer({
resourceId: 'local-resource-unresolved',
resourcePersistenceState: 'unresolved-local',
});
renderContextMenus({
contextMenu: {
kind: 'layer',
x: 16,
y: 18,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
contextMenuLayer: layer,
canCopyContextLayers: false,
contextLayerCopyBlockReason: 'unresolved-resource',
});
expect(
screen.getByRole('menuitem', { name: '复制' }).getAttribute('title'),
).toBe('素材资源尚未登记,暂不能复制');
});
it('keeps measured layer menus and submenus inside the viewport', async () => {
const originalInnerWidth = window.innerWidth;
const originalInnerHeight = window.innerHeight;
window.innerWidth = 500;
window.innerHeight = 640;
const getBoundingClientRectSpy = vi
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function getBoundingClientRect(this: HTMLElement) {
if (this.classList.contains('image-canvas-editor__context-menu')) {
return createDomRect(188, 620);
}
if (
this.classList.contains('image-canvas-editor__context-submenu-panel')
) {
return createDomRect(160, 72);
}
if (this.classList.contains('image-canvas-editor__context-submenu')) {
return createDomRect(188, 29, { top: 586 });
}
return createDomRect(0, 0);
});
try {
const layer = createLayer({ mediaType: 'image-sequence' });
renderContextMenus({
contextMenu: {
kind: 'layer',
x: 490,
y: 500,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
contextMenuLayer: layer,
});
const menu = screen.getByRole('menu', { name: '图片功能面板' });
await waitFor(() => {
expect(menu.style.left).toBe('304px');
expect(menu.style.top).toBe('12px');
expect(menu.className).toContain(
'image-canvas-editor__context-menu--submenu-left',
);
expect(menu.className).toContain(
'image-canvas-editor__context-menu--submenu-up',
);
});
} finally {
getBoundingClientRectSpy.mockRestore();
window.innerWidth = originalInnerWidth;
window.innerHeight = originalInnerHeight;
}
});
it('closes the standalone image menu after opening character animation', () => {
const layer = createLayer({ assetKind: 'character' });
const props = renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
fireEvent.click(screen.getByRole('menuitem', { name: '生成动画' }));
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(1);
});
it('renders the image context menu when no canvas context menu is open', () => {
const layer = createLayer({ assetKind: 'character' });
const props = renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
fireEvent.click(screen.getByRole('menuitem', { name: '快速编辑' }));
fireEvent.click(screen.getByRole('menuitem', { name: '查看图片信息' }));
fireEvent.click(screen.getByRole('menuitem', { name: '删除图片' }));
expect(props.onOpenQuickEditPanel).toHaveBeenCalledWith(layer);
expect(props.onOpenLayerMetadata).toHaveBeenCalledWith(layer);
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
expect(props.onDeleteLayerById).toHaveBeenCalledWith(layer.id);
});
it.each([
{
title: '角色动作',
mediaType: 'image-sequence' as const,
assetKind: 'character-animation' as const,
},
{
title: '点击音效',
mediaType: 'audio' as const,
assetKind: 'sound-effect' as const,
},
{
title: '背景音乐',
mediaType: 'audio' as const,
assetKind: 'background-music' as const,
},
{
title: '视频',
mediaType: 'video' as const,
assetKind: 'video' as const,
},
])('hides quick edit from both menus for $title', (overrides) => {
const layer = createLayer(overrides);
renderContextMenus({
contextMenu: {
kind: 'layer',
x: 16,
y: 18,
layerId: layer.id,
canvasPoint: { x: 40, y: 42 },
},
imageContextMenuLayer: layer,
contextMenuLayer: layer,
});
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
cleanup();
renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
});
it('hides quick edit from the standalone menu for individual icons', () => {
const layer = createLayer({ assetKind: 'icon' });
renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull();
expect(screen.getByRole('menuitem', { name: '查看图片信息' })).toBeTruthy();
});
it('keeps quick edit in the standalone menu for icon spritesheets', () => {
const layer = createLayer({ assetKind: 'icon-spritesheet' });
renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
expect(screen.getByRole('menuitem', { name: '快速编辑' })).toBeTruthy();
});
it('keeps quick edit in the standalone menu for icon specs', () => {
const layer = createLayer({ assetKind: 'icon-spec' });
renderContextMenus({
imageContextMenu: { layerId: layer.id, x: 20, y: 22 },
imageContextMenuLayer: layer,
});
expect(screen.getByRole('menuitem', { name: '快速编辑' })).toBeTruthy();
});
});