Files
Genarrative/apps/ai-game-creator-shell/tests/previewWorkspaceZoom.test.tsx
T
k88936 7288c6f641
Project CI / Repository checks (push) Successful in 2m59s
Project CI / Frontend tests (push) Successful in 3m29s
Project CI / Backend tests (push) Successful in 8m37s
Project CI / Native shell tests (push) Successful in 20m55s
Fix/修复无限画布图标文字模糊 (#285)
closes #279
closes #284

实现: 从2(暂定)倍放大绘制的画布scale 0.5* 真正的scale

before:
![shotmd-1788579338.jpg](/attachments/9d44ec5e-454c-449b-86bc-b2131fbbc8cc)
![shotmd-1788578910.jpg](/attachments/c9da61a2-b467-43eb-a5de-e5e4d5cb8f70)

after:
![shotmd-1788580930.jpg](/attachments/f1f06e8e-90bf-45e2-a30b-a63ccc308c19)
![shotmd-1788583400.jpg](/attachments/c48ca741-e470-42b3-8030-064e0579f63a)
svg看起来stroke窄了一点点, 可以接受

Reviewed-on: #285
2026-09-09 16:32:43 +08:00

210 lines
5.6 KiB
TypeScript

// @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',
components_status: 'NoProblem',
allow_llm_edit_layout: true,
allow_llm_edit_component: true,
source: 'System',
},
components: [],
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> = {},
): 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(<PreviewWorkspace canvas={createCanvas()} />);
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(<PreviewWorkspace canvas={createCanvas()} />);
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 actual-size and fit shortcuts within the preview scope', () => {
const rendered = render(<PreviewWorkspace canvas={createCanvas()} />);
const preview = screen.getByRole('region', { name: 'UI 预览画布' });
const fitted = logicalViewportScale(rendered.container);
fireEvent.focus(preview);
fireEvent.keyDown(window, {
key: '1',
ctrlKey: true,
cancelable: true,
});
expect(logicalViewportScale(rendered.container)).toBe(1);
fireEvent.keyDown(window, {
key: '0',
ctrlKey: true,
cancelable: true,
});
expect(logicalViewportScale(rendered.container)).toBe(fitted);
});
it('leaves browser zoom untouched when the preview has no content', () => {
render(
<PreviewWorkspace
canvas={createCanvas({
activeImage: null,
activeImageId: null,
tree: null,
})}
/>,
);
expect(
fireEvent.keyDown(window, {
key: '=',
ctrlKey: true,
cancelable: true,
}),
).toBe(true);
});
});