// @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Node as UiNode } from '../src/features/ui-editor/types/Node'; import type { UIDesignImage } from '../src/features/ui-editor/types/UIDesignImage'; import { PreviewWorkspace } from '../src/view/ui-editor/components/preview/PreviewWorkspace'; import type { UiEditorCanvasProjection } from '../src/view/ui-editor/useUiEditorPage'; class TestResizeObserver { observe() {} disconnect() {} unobserve() {} } const root: UiNode = { id: 'root', layout: { transform: { anchor_min: [0, 0], anchor_max: [1, 1], offset_min: [0, 0], offset_max: [0, 0], }, custom_minimum_size: [0, 0], size_flags_horizontal: 1, size_flags_vertical: 1, size_flags_stretch_ratio: 1, container: 'None', }, metadata: { name: '根节点', description: '', layout_status: 'NoProblem', component_status: 'NoProblem', allow_llm_edit_layout: true, allow_llm_edit_component: true, source: 'System', }, component: null, children_display_mode: 'Stack', children: [], }; const activeImage: UIDesignImage = { metadata: { name: '测试界面', description: '', role: null, slave_to: null, }, path: 'assets/page.png', pixel_size: [1200, 800], pixels_per_unit: 1, }; function createCanvas( overrides: Partial = {}, ): UiEditorCanvasProjection { return { isLocked: false, activeImage, activeImageId: 'page-1', previewUrls: { 'page-1': 'data:image/png;base64,' }, images: { 'page-1': activeImage }, sprites: {}, fontFaces: {}, tree: { src_ui_design: 'page-1', root }, selectedNode: null, selectedNodeId: null, keepChildrenUnchanged: false, hiddenNodeIds: new Set(), focusRequest: null, status: null, isNodePreviewVisible: vi.fn(() => true), toggleNodePreviewVisibility: vi.fn(), selectExclusiveChild: vi.fn(), selectNode: vi.fn(), clearNodeSelection: vi.fn(), updateNodeTransform: vi.fn(), insertNode: vi.fn(), insertNodeAfter: vi.fn(), deleteNode: vi.fn(), openClearDialog: vi.fn(), ...overrides, }; } function logicalViewportScale(container: HTMLElement) { const world = container.querySelector( '.genarrative-image-canvas__world', ) as HTMLElement; const inverseScale = Number( world.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'), ); if (!Number.isFinite(inverseScale) || inverseScale <= 0) { throw new Error(`无法从 world 读取逻辑缩放值`); } return 1 / inverseScale; } beforeEach(() => { vi.stubGlobal('ResizeObserver', TestResizeObserver); }); afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks(); }); describe('PreviewWorkspace quick zoom', () => { it('uses the toolbar zoom step while the preview is hovered', () => { const rendered = render(); const preview = screen.getByRole('region', { name: 'UI 预览画布' }); const before = logicalViewportScale(rendered.container); fireEvent.pointerEnter(preview); const wasNotCancelled = fireEvent.keyDown(window, { key: '=', code: 'Equal', ctrlKey: true, cancelable: true, }); expect(wasNotCancelled).toBe(false); expect(logicalViewportScale(rendered.container)).toBeCloseTo(before * 1.16); }); it('requires hover or focus and ignores interactive targets', () => { const rendered = render(); const preview = screen.getByRole('region', { name: 'UI 预览画布' }); const zoomButton = screen.getByRole('button', { name: '放大画布' }); const initial = logicalViewportScale(rendered.container); expect( fireEvent.keyDown(window, { key: '=', ctrlKey: true, cancelable: true, }), ).toBe(true); expect(logicalViewportScale(rendered.container)).toBe(initial); fireEvent.pointerEnter(preview); expect( fireEvent.keyDown(zoomButton, { key: '=', ctrlKey: true, cancelable: true, }), ).toBe(true); expect(logicalViewportScale(rendered.container)).toBe(initial); fireEvent.pointerLeave(preview); fireEvent.focus(preview); fireEvent.keyDown(window, { key: '-', code: 'Minus', ctrlKey: true, cancelable: true, }); expect(logicalViewportScale(rendered.container)).toBeCloseTo( initial * 0.86, ); }); it('keeps the actual-size shortcut within the preview scope', () => { const rendered = render(); const preview = screen.getByRole('region', { name: 'UI 预览画布' }); fireEvent.focus(preview); fireEvent.keyDown(window, { key: '1', ctrlKey: true, cancelable: true, }); expect(logicalViewportScale(rendered.container)).toBe(1); }); it('refits the preview when the zoom percentage button is clicked', () => { const rendered = render(); const fitButton = screen.getByRole('button', { name: '适配画布' }); const zoomInButton = screen.getByRole('button', { name: '放大画布' }); const initial = logicalViewportScale(rendered.container); fireEvent.click(zoomInButton); expect(logicalViewportScale(rendered.container)).toBeCloseTo( initial * 1.16, ); fireEvent.click(fitButton); expect(logicalViewportScale(rendered.container)).toBeCloseTo(initial); }); it('leaves browser zoom untouched when the preview has no content', () => { render( , ); expect( fireEvent.keyDown(window, { key: '=', ctrlKey: true, cancelable: true, }), ).toBe(true); }); });