/* @vitest-environment jsdom */ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import type { CanvasGenerationDialogState, CanvasLayer, } from './ImageCanvasEditorTypes'; import { ImageCanvasWorldView } from './ImageCanvasWorldView'; function createLayer(overrides: Partial = {}): 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 { 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[0]> = {}, ) { const props: Parameters[0] = { viewport: { x: 10, y: 20, scale: 1.5 }, snapGuide: null, layers: [createLayer()], selectedLayerIds: [], hoveredLayerId: null, canvasMarquee: null, canvasGenerationDialogs: [], generateDialog: null, quickEditPanel: 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(), ...overrides, }; const result = render(); 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, quickEditPanel: { sourceLayerId: visibleLayer.id, prompt: '快速编辑', size: '1024x1024', model: 'gpt-image-2', status: 'generating', }, }); 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).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.getByRole('status', { name: '' }).textContent).toBe('生成中'); }); 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, }), ], }); const frame = screen.getByRole('button', { name: '图标素材生成占位图' }); 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('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(); }); });