Files
Genarrative/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
T
kdletters 9807295a7d 调整画布生成输入与图集快速编辑
隐藏生成面板资源名称字段并恢复提示词输入边框

恢复图标图集快速编辑并仅对单图标禁用

补充工具栏右键菜单工作流回归测试与编辑器文档
2026-07-14 23:26:35 +08:00

302 lines
10 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 { 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,
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)' }),
);
fireEvent.click(screen.getByRole('menuitem', { name: '快速编辑' }));
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).toHaveBeenCalledWith(layer);
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(2);
expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2);
expect(props.onDeleteContextLayers).toHaveBeenCalledTimes(1);
});
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('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();
});
});