/* @vitest-environment jsdom */ import { fireEvent, render, screen } from '@testing-library/react'; import { useState } from 'react'; import { describe, expect, it, vi } from 'vitest'; import type { CanvasContextMenuState, CanvasLayer, } from './ImageCanvasEditorTypes'; import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands'; function createLayer(id: string, x: number, zIndex: number): CanvasLayer { return { id, resourceId: `resource-${id}`, title: id, src: `data:image/png;base64,${id}`, x, y: 20, width: 100, height: 80, originalWidth: 100, originalHeight: 80, zIndex, sourceType: 'uploaded', }; } function LayerCommandsHarness({ exportLayerImage = vi.fn(), onDeleteLayerSideEffects = vi.fn(), }: { exportLayerImage?: (layer: CanvasLayer | null) => void; onDeleteLayerSideEffects?: (layerId: string) => void; }) { const [layers, setLayers] = useState([ createLayer('first', 10, 1), createLayer('second', 160, 2), createLayer('third', 310, 3), ]); const [selectedLayerId, setSelectedLayerId] = useState( 'first', ); const [selectedLayerIds, setSelectedLayerIds] = useState([ 'first', 'second', ]); const [metadataLayer, setMetadataLayer] = useState( layers[0] ?? null, ); const [hoveredLayerId, setHoveredLayerId] = useState('first'); const [contextMenu, setContextMenu] = useState( { kind: 'layer', layerId: 'first', x: 0, y: 0, canvasPoint: { x: 0, y: 0 }, }, ); const [historyCount, setHistoryCount] = useState(0); const [imageContextClosedCount, setImageContextClosedCount] = useState(0); const [activeTool, setActiveTool] = useState('shape'); const selectSingleLayer = (layerId: string | null) => { setSelectedLayerId(layerId); setSelectedLayerIds(layerId ? [layerId] : []); }; const commands = useImageCanvasLayerCommands({ layers, contextMenu, selectedLayerId, selectedLayerIds, setLayers, setSelectedLayerId, setSelectedLayerIds, setHoveredLayerId, setMetadataLayer, setContextMenu, setImageContextMenu: () => setImageContextClosedCount((currentCount) => currentCount + 1), setActiveTool, captureCanvasHistory: () => setHistoryCount((currentCount) => currentCount + 1), selectSingleLayer, onDeleteLayerSideEffects, exportLayerImage, }); return (
{layers .map( (layer) => `${layer.id}:${layer.title}:${layer.x}:${layer.zIndex}:${layer.groupId ?? '-'}:${layer.hidden ? 'hidden' : 'shown'}:${layer.locked ? 'locked' : 'open'}:${layer.flipX ? 'flipX' : '-'}:${layer.flipY ? 'flipY' : '-'}`, ) .join('|')} {selectedLayerId ?? '-'}:{selectedLayerIds.join(',')} {metadataLayer?.id ?? '-'} {hoveredLayerId ?? '-'} {contextMenu ? 'open' : 'closed'} {historyCount} {imageContextClosedCount} {activeTool} {commands.canvasClipboard ? `${commands.canvasClipboard.mode}:${commands.canvasClipboard.layers.length}` : '-'}
); } describe('useImageCanvasLayerCommands', () => { it('copies, cuts, and pastes context layers with history and selection updates', () => { render(); fireEvent.click(screen.getByRole('button', { name: '复制' })); expect(screen.getByTestId('clipboard').textContent).toBe('copy:2'); expect(screen.getByTestId('context').textContent).toBe('closed'); fireEvent.click(screen.getByRole('button', { name: '粘贴' })); expect(screen.getByTestId('layers').textContent).toContain( 'first 副本:500:4', ); expect(screen.getByTestId('layers').textContent).toContain( 'second 副本:650:5', ); expect(screen.getByTestId('selection').textContent).toContain( 'layer-copy-', ); expect(screen.getByTestId('tool').textContent).toBe('select'); fireEvent.click(screen.getByRole('button', { name: '右键第三层' })); fireEvent.click(screen.getByRole('button', { name: '只选第三层' })); fireEvent.click(screen.getByRole('button', { name: '剪切' })); expect(screen.getByTestId('clipboard').textContent).toBe('cut:1'); expect(screen.getByTestId('layers').textContent).not.toContain('third'); expect(screen.getByTestId('selection').textContent).toBe('-:'); expect(Number(screen.getByTestId('history').textContent)).toBeGreaterThan( 1, ); }); it('applies layer commands and clears menus without owning menu positioning', () => { const exportLayerImage = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: '创建组' })); expect(screen.getByTestId('layers').textContent).toContain('layer-group-'); fireEvent.click(screen.getByRole('button', { name: '右键第三层' })); fireEvent.click(screen.getByRole('button', { name: '显隐' })); expect(screen.getByTestId('layers').textContent).toContain(':hidden:'); fireEvent.click(screen.getByRole('button', { name: '右键第三层' })); fireEvent.click(screen.getByRole('button', { name: '锁定' })); expect(screen.getByTestId('layers').textContent).toContain(':locked:'); fireEvent.click(screen.getByRole('button', { name: '右键第三层' })); fireEvent.click(screen.getByRole('button', { name: '翻转' })); expect(screen.getByTestId('layers').textContent).toContain(':flipX:'); fireEvent.click(screen.getByRole('button', { name: '右键第三层' })); fireEvent.click(screen.getByRole('button', { name: '导出' })); expect(exportLayerImage).toHaveBeenCalledWith( expect.objectContaining({ id: 'third' }), ); expect(screen.getByTestId('context').textContent).toBe('closed'); }); it('deletes selected and direct layers while running delete side effects', () => { const onDeleteLayerSideEffects = vi.fn(); render( , ); fireEvent.click(screen.getByRole('button', { name: '删除选中' })); expect(screen.getByTestId('layers').textContent).not.toContain('first'); expect(screen.getByTestId('layers').textContent).not.toContain('second'); expect(screen.getByTestId('selection').textContent).toBe('third:third'); expect(screen.getByTestId('metadata').textContent).toBe('-'); expect(screen.getByTestId('image-context-closed').textContent).toBe('1'); expect(onDeleteLayerSideEffects).toHaveBeenCalledWith('first'); expect(onDeleteLayerSideEffects).toHaveBeenCalledWith('second'); fireEvent.click(screen.getByRole('button', { name: '删除单图' })); expect(screen.getByTestId('image-context-closed').textContent).toBe('2'); }); });