修复 AI 游戏创作壳测试解析
显式解析文件管理器已发布的 ESM 入口 迁移 UI 编辑器测试到会话投影与几何模块 修正节点交互测试的画布接口契约
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveProportionalResize,
|
||||
resizePageRect,
|
||||
resolveProportionalResizeAxis,
|
||||
} from '../src/view/ui-editor/components/preview/proportionalResize';
|
||||
} from '../src/features/ui-editor/nodeTransformGeometry';
|
||||
|
||||
describe('proportional resize', () => {
|
||||
it('selects the axis with the greater relative size change', () => {
|
||||
@@ -17,23 +17,25 @@ describe('proportional resize', () => {
|
||||
|
||||
it('uses the supplied locked axis instead of the larger candidate size', () => {
|
||||
expect(
|
||||
resolveProportionalResize({
|
||||
startSize: [100, 100],
|
||||
size: [150, 90],
|
||||
delta: [50, -10],
|
||||
axis: 'horizontal',
|
||||
}),
|
||||
).toEqual({ axis: 'horizontal', size: [150, 150] });
|
||||
resizePageRect(
|
||||
{ min: [0, 0], max: [100, 100] },
|
||||
'se',
|
||||
[50, -10],
|
||||
true,
|
||||
'horizontal',
|
||||
),
|
||||
).toEqual({ min: [0, 0], max: [150, 150] });
|
||||
});
|
||||
|
||||
it('uses the locked vertical axis for an opposite-sign diagonal drag', () => {
|
||||
expect(
|
||||
resolveProportionalResize({
|
||||
startSize: [100, 100],
|
||||
size: [110, 50],
|
||||
delta: [10, -50],
|
||||
axis: 'vertical',
|
||||
}),
|
||||
).toEqual({ axis: 'vertical', size: [50, 50] });
|
||||
resizePageRect(
|
||||
{ min: [0, 0], max: [100, 100] },
|
||||
'se',
|
||||
[10, -50],
|
||||
true,
|
||||
'vertical',
|
||||
),
|
||||
).toEqual({ min: [0, 0], max: [50, 50] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,12 +25,22 @@ vi.mock('../src/components/modal/ThemedModal', () => ({
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import type { Node as UiNode } from '../src/features/ui-editor/types/Node';
|
||||
import type { State } from '../src/features/ui-editor/types/State';
|
||||
import type {
|
||||
UiDesignStateSnapshot,
|
||||
UiDesignStateStore,
|
||||
} from '../src/features/ui-editor/uiDesignStateStore';
|
||||
import UiEditorPage from '../src/view/ui-editor';
|
||||
import { useUiEditorPage } from '../src/view/ui-editor/useUiEditorPage';
|
||||
import { useUiEditorSession } from '../src/view/ui-editor/useUiEditorPage';
|
||||
|
||||
class TestResizeObserver {
|
||||
constructor(_callback: ResizeObserverCallback) {}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
vi.stubGlobal('ResizeObserver', TestResizeObserver);
|
||||
|
||||
const EMPTY_SNAPSHOT: UiDesignStateSnapshot = {
|
||||
revision: 0,
|
||||
@@ -42,6 +52,98 @@ const EMPTY_SNAPSHOT: UiDesignStateSnapshot = {
|
||||
},
|
||||
};
|
||||
|
||||
function node(id: string, children: UiNode[] = []): UiNode {
|
||||
return {
|
||||
id,
|
||||
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: id,
|
||||
description: '',
|
||||
layout_status: 'NoProblem',
|
||||
components_status: 'NoProblem',
|
||||
allow_llm_edit_layout: true,
|
||||
allow_llm_edit_component: true,
|
||||
source: 'System',
|
||||
},
|
||||
components: [],
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function stateWithPages(pageIds: string[]): State {
|
||||
return {
|
||||
ui_design_images: Object.fromEntries(
|
||||
pageIds.map((id) => [
|
||||
id,
|
||||
{
|
||||
metadata: {
|
||||
name: id,
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: `assets/${id}.png`,
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
]),
|
||||
),
|
||||
ui_trees: pageIds.map((id) => ({
|
||||
src_ui_design: id,
|
||||
root: node(`${id}-root`),
|
||||
})),
|
||||
sprite_assets: {
|
||||
panel: {
|
||||
asset_id: 'panel',
|
||||
metadata: { name: 'Panel', asset_type: '' },
|
||||
path: 'assets/panel.png',
|
||||
pixel_size: [32, 32],
|
||||
pixels_per_unit: 1,
|
||||
border: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
},
|
||||
},
|
||||
font_assets: {
|
||||
body: {
|
||||
asset_id: 'body',
|
||||
metadata: {
|
||||
family_name: '测试字体',
|
||||
face_name: 'Regular',
|
||||
weight: 400,
|
||||
italic: false,
|
||||
format: 'TrueType',
|
||||
source_file_name: 'body.ttf',
|
||||
},
|
||||
path: 'assets/fonts/body.ttf',
|
||||
content_sha256: 'a'.repeat(64),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function renderLoadedSession(state: State) {
|
||||
const stateStore: UiDesignStateStore = {
|
||||
load: vi.fn().mockResolvedValue({ revision: 0, state }),
|
||||
save: vi.fn(),
|
||||
};
|
||||
const hook = renderHook(() =>
|
||||
useUiEditorSession('/tmp/ui-editor', 'ui-resource', stateStore),
|
||||
);
|
||||
await waitFor(() => expect(hook.result.current.save.isLoading).toBe(false));
|
||||
return hook;
|
||||
}
|
||||
|
||||
describe('UiEditorPage', () => {
|
||||
it('uses the durable Tauri store for a project UI resource', async () => {
|
||||
vi.mocked(invoke)
|
||||
@@ -155,242 +257,140 @@ describe('UiEditorPage', () => {
|
||||
expect(screen.getByText('素材导入器')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('switches from a selected node to the sprite inspector', () => {
|
||||
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
|
||||
it('switches from a selected node to the sprite inspector', async () => {
|
||||
const { result } = await renderLoadedSession(stateWithPages(['page']));
|
||||
const rootId = 'page-root';
|
||||
act(() => {
|
||||
result.current.editor.addDesignImages([
|
||||
{
|
||||
id: 'page',
|
||||
image: {
|
||||
metadata: {
|
||||
name: 'Page',
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page.png',
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
result.current.editor.addSpriteAssets([
|
||||
{
|
||||
asset_id: 'panel',
|
||||
metadata: { name: 'Panel', asset_type: '' },
|
||||
path: 'assets/panel.png',
|
||||
pixel_size: [32, 32],
|
||||
pixels_per_unit: 1,
|
||||
border: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
},
|
||||
]);
|
||||
result.current.editor.addFontAssets([
|
||||
{
|
||||
asset_id: 'body',
|
||||
metadata: {
|
||||
family_name: '测试字体',
|
||||
face_name: 'Regular',
|
||||
weight: 400,
|
||||
italic: false,
|
||||
format: 'TrueType',
|
||||
source_file_name: 'body.ttf',
|
||||
},
|
||||
path: 'assets/fonts/body.ttf',
|
||||
content_sha256: 'a'.repeat(64),
|
||||
},
|
||||
]);
|
||||
result.current.input.selectDesignImage('page');
|
||||
result.current.input.selectNode(rootId);
|
||||
});
|
||||
const rootId = result.current.editor.state.ui_trees[0]!.root.id;
|
||||
act(() => {
|
||||
result.current.selectDesignImage('page');
|
||||
result.current.selectNode(rootId);
|
||||
});
|
||||
expect(result.current.selectedNodeId).toBe(rootId);
|
||||
expect(result.current.canvas.selectedNodeId).toBe(rootId);
|
||||
|
||||
act(() => result.current.selectSprite('panel'));
|
||||
expect(result.current.selectedNodeId).toBeNull();
|
||||
expect(result.current.selectedSpriteId).toBe('panel');
|
||||
act(() => result.current.input.selectSprite('panel'));
|
||||
expect(result.current.canvas.selectedNodeId).toBeNull();
|
||||
expect(result.current.inspector.selectedSpriteId).toBe('panel');
|
||||
|
||||
act(() => result.current.selectFont('body'));
|
||||
expect(result.current.selectedFontId).toBe('body');
|
||||
act(() => result.current.input.selectFont('body'));
|
||||
expect(result.current.inspector.selectedFontId).toBe('body');
|
||||
|
||||
act(() => result.current.selectDesignImage('page'));
|
||||
expect(result.current.selectedFontId).toBeNull();
|
||||
act(() => result.current.input.selectDesignImage('page'));
|
||||
expect(result.current.inspector.selectedFontId).toBeNull();
|
||||
|
||||
act(() => result.current.selectFont('body'));
|
||||
act(() => result.current.selectSprite('panel'));
|
||||
expect(result.current.selectedFontId).toBeNull();
|
||||
act(() => result.current.input.selectFont('body'));
|
||||
act(() => result.current.input.selectSprite('panel'));
|
||||
expect(result.current.inspector.selectedFontId).toBeNull();
|
||||
|
||||
act(() => result.current.selectFont('body'));
|
||||
act(() => result.current.selectNode(rootId));
|
||||
expect(result.current.selectedFontId).toBeNull();
|
||||
act(() => result.current.input.selectFont('body'));
|
||||
act(() => result.current.input.selectNode(rootId));
|
||||
expect(result.current.inspector.selectedFontId).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps node preview visibility transient and attached to global node IDs', () => {
|
||||
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
|
||||
act(() => {
|
||||
result.current.editor.addDesignImages([
|
||||
{
|
||||
id: 'page-a',
|
||||
image: {
|
||||
metadata: {
|
||||
name: 'Page A',
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page-a.png',
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'page-b',
|
||||
image: {
|
||||
metadata: {
|
||||
name: 'Page B',
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page-b.png',
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
act(() => result.current.selectDesignImage('page-a'));
|
||||
const rootId = result.current.treeForActiveImage!.root.id;
|
||||
it('keeps node preview visibility transient and attached to global node IDs', async () => {
|
||||
const { result } = await renderLoadedSession(
|
||||
stateWithPages(['page-a', 'page-b']),
|
||||
);
|
||||
const rootId = 'page-a-root';
|
||||
let insertedNodeId: string | undefined;
|
||||
act(() => {
|
||||
const inserted = result.current.insertNode(rootId);
|
||||
const inserted = result.current.input.insertNode(rootId, 'page-a');
|
||||
if (inserted?.ok) insertedNodeId = inserted.value;
|
||||
});
|
||||
const stateBeforeToggle = JSON.stringify(result.current.editor.state);
|
||||
const stateBeforeToggle = JSON.stringify(result.current.canvas.tree);
|
||||
act(() => {
|
||||
result.current.toggleNodePreviewVisibility(rootId);
|
||||
result.current.canvas.toggleNodePreviewVisibility(rootId);
|
||||
});
|
||||
expect(result.current.isNodePreviewVisible(rootId)).toBe(false);
|
||||
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
expect(JSON.stringify(result.current.editor.state)).toBe(stateBeforeToggle);
|
||||
expect(result.current.canvas.isNodePreviewVisible(rootId)).toBe(false);
|
||||
expect(result.current.canvas.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
expect(JSON.stringify(result.current.canvas.tree)).toBe(stateBeforeToggle);
|
||||
|
||||
act(() => result.current.selectDesignImage('page-b'));
|
||||
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
act(() => result.current.input.selectDesignImage('page-b'));
|
||||
expect(result.current.canvas.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
|
||||
act(() => result.current.selectDesignImage('page-a'));
|
||||
expect(result.current.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
act(() => result.current.input.selectDesignImage('page-a'));
|
||||
expect(result.current.canvas.hiddenNodeIds.has(rootId)).toBe(true);
|
||||
expect(insertedNodeId).toBeTruthy();
|
||||
act(() => result.current.toggleNodePreviewVisibility(insertedNodeId!));
|
||||
expect(result.current.hiddenNodeIds.has(insertedNodeId!)).toBe(true);
|
||||
act(() =>
|
||||
result.current.canvas.toggleNodePreviewVisibility(insertedNodeId!),
|
||||
);
|
||||
expect(result.current.canvas.hiddenNodeIds.has(insertedNodeId!)).toBe(true);
|
||||
|
||||
act(() => result.current.deleteNode(insertedNodeId!));
|
||||
expect(result.current.hiddenNodeIds.has(insertedNodeId!)).toBe(false);
|
||||
act(() => result.current.input.deleteNode(insertedNodeId!, 'page-a'));
|
||||
expect(result.current.canvas.hiddenNodeIds.has(insertedNodeId!)).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
act(() => result.current.clearState());
|
||||
expect(result.current.hiddenNodeIds.size).toBe(0);
|
||||
act(() => result.current.dialogs.clearState());
|
||||
expect(result.current.canvas.hiddenNodeIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps Inspector status highlighting separate from node navigation', () => {
|
||||
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
|
||||
act(() => {
|
||||
result.current.editor.addDesignImages([
|
||||
{
|
||||
id: 'page',
|
||||
image: {
|
||||
metadata: {
|
||||
name: 'Page',
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page.png',
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
result.current.selectDesignImage('page');
|
||||
});
|
||||
const rootId = result.current.treeForActiveImage!.root.id;
|
||||
it('keeps Inspector status highlighting separate from node navigation', async () => {
|
||||
const { result } = await renderLoadedSession(stateWithPages(['page']));
|
||||
const rootId = 'page-root';
|
||||
let otherNodeId: string | undefined;
|
||||
act(() => {
|
||||
otherNodeId = result.current.insertNode(rootId)?.value;
|
||||
result.current.selectNode(rootId);
|
||||
result.current.highlightStatusField('layout_status');
|
||||
otherNodeId = result.current.input.insertNode(rootId, 'page')?.value;
|
||||
result.current.input.selectNode(rootId);
|
||||
result.current.input.highlightStatusField('layout_status');
|
||||
});
|
||||
expect(result.current.highlightedStatusField).toMatchObject({
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
field: 'layout_status',
|
||||
requestId: 1,
|
||||
});
|
||||
|
||||
act(() => result.current.highlightStatusField('layout_status'));
|
||||
expect(result.current.highlightedStatusField).toMatchObject({
|
||||
act(() => result.current.input.highlightStatusField('layout_status'));
|
||||
expect(result.current.inspector.highlightedStatusField).toMatchObject({
|
||||
field: 'layout_status',
|
||||
requestId: 2,
|
||||
});
|
||||
|
||||
act(() => result.current.selectNode(otherNodeId!));
|
||||
expect(result.current.highlightedStatusField).toBeNull();
|
||||
act(() => result.current.input.selectNode(rootId));
|
||||
expect(result.current.inspector.highlightedStatusField).toBeNull();
|
||||
|
||||
act(() => {
|
||||
result.current.highlightStatusField('components_status');
|
||||
result.current.setNodeMetadata({ components_status: 'NoProblem' });
|
||||
result.current.input.highlightStatusField('components_status');
|
||||
result.current.inspector.setNodeMetadata({
|
||||
components_status: 'NoProblem',
|
||||
});
|
||||
});
|
||||
expect(result.current.highlightedStatusField).toBeNull();
|
||||
expect(result.current.inspector.highlightedStatusField).toBeNull();
|
||||
});
|
||||
|
||||
it('shares exclusive child visibility between tree actions and final preview state', () => {
|
||||
const { result } = renderHook(() => useUiEditorPage('/tmp/ui-editor'));
|
||||
act(() => {
|
||||
result.current.editor.addDesignImages([
|
||||
{
|
||||
id: 'page',
|
||||
image: {
|
||||
metadata: {
|
||||
name: 'Page',
|
||||
description: '',
|
||||
role: 'Page',
|
||||
slave_to: null,
|
||||
},
|
||||
path: 'assets/page.png',
|
||||
pixel_size: [320, 180],
|
||||
pixels_per_unit: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
result.current.selectDesignImage('page');
|
||||
});
|
||||
const rootId = result.current.treeForActiveImage!.root.id;
|
||||
it('shares exclusive child visibility between tree actions and final preview state', async () => {
|
||||
const { result } = await renderLoadedSession(stateWithPages(['page']));
|
||||
const rootId = 'page-root';
|
||||
let parentId: string | undefined;
|
||||
let childAId: string | undefined;
|
||||
let childBId: string | undefined;
|
||||
let childBDescendantId: string | undefined;
|
||||
act(() => {
|
||||
parentId = result.current.insertNode(rootId)?.value;
|
||||
childAId = result.current.insertNode(parentId!)?.value;
|
||||
childBId = result.current.insertNode(parentId!)?.value;
|
||||
childBDescendantId = result.current.insertNode(childBId!)?.value;
|
||||
parentId = result.current.input.insertNode(rootId, 'page')?.value;
|
||||
childAId = result.current.input.insertNode(parentId!, 'page')?.value;
|
||||
childBId = result.current.input.insertNode(parentId!, 'page')?.value;
|
||||
childBDescendantId = result.current.input.insertNode(
|
||||
childBId!,
|
||||
'page',
|
||||
)?.value;
|
||||
});
|
||||
act(() => result.current.selectNode(parentId!));
|
||||
act(() => result.current.setNodeChildrenDisplayMode('Exclusive'));
|
||||
act(() => result.current.input.selectNode(parentId!));
|
||||
act(() => result.current.inspector.setNodeChildrenDisplayMode('Exclusive'));
|
||||
|
||||
expect(parentId && childAId && childBId).toBeTruthy();
|
||||
expect(result.current.isNodePreviewVisible(childAId!)).toBe(true);
|
||||
expect(result.current.isNodePreviewVisible(childBId!)).toBe(false);
|
||||
expect(result.current.isNodePreviewVisible(childBDescendantId!)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(result.current.canvas.isNodePreviewVisible(childAId!)).toBe(true);
|
||||
expect(result.current.canvas.isNodePreviewVisible(childBId!)).toBe(false);
|
||||
expect(
|
||||
result.current.canvas.isNodePreviewVisible(childBDescendantId!),
|
||||
).toBe(false);
|
||||
|
||||
act(() => result.current.toggleNodePreviewVisibility(childAId!));
|
||||
expect(result.current.isNodePreviewVisible(childAId!)).toBe(false);
|
||||
expect(result.current.isNodePreviewVisible(childBId!)).toBe(false);
|
||||
act(() => result.current.canvas.toggleNodePreviewVisibility(childAId!));
|
||||
expect(result.current.canvas.isNodePreviewVisible(childAId!)).toBe(false);
|
||||
expect(result.current.canvas.isNodePreviewVisible(childBId!)).toBe(false);
|
||||
|
||||
act(() => result.current.toggleNodePreviewVisibility(childBId!));
|
||||
expect(result.current.isNodePreviewVisible(childAId!)).toBe(false);
|
||||
expect(result.current.isNodePreviewVisible(childBId!)).toBe(true);
|
||||
expect(result.current.isNodePreviewVisible(childBDescendantId!)).toBe(true);
|
||||
act(() => result.current.canvas.toggleNodePreviewVisibility(childBId!));
|
||||
expect(result.current.canvas.isNodePreviewVisible(childAId!)).toBe(false);
|
||||
expect(result.current.canvas.isNodePreviewVisible(childBId!)).toBe(true);
|
||||
expect(
|
||||
result.current.canvas.isNodePreviewVisible(childBDescendantId!),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('shows a page-level save failure and allows a retry', async () => {
|
||||
@@ -410,7 +410,7 @@ describe('UiEditorPage', () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: '仍然保存' }));
|
||||
|
||||
expect((await screen.findByRole('alert')).textContent).toContain(
|
||||
'保存失败,请稍后重试。',
|
||||
'临时存储不可用',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: '保存' })).toBeTruthy();
|
||||
expect(stateStore.save).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import type { Node as UiNode } from '../src/features/ui-editor/types/Node';
|
||||
import type { UITree } from '../src/features/ui-editor/types/UITree';
|
||||
import { useNodeTransformInteraction } from '../src/view/ui-editor/components/preview/useNodeTransformInteraction';
|
||||
import type { UiEditorPageController } from '../src/view/ui-editor/useUiEditorPage';
|
||||
import type { UiEditorCanvasProjection } from '../src/view/ui-editor/useUiEditorPage';
|
||||
|
||||
function node(id: string, children: UiNode[] = []): UiNode {
|
||||
return {
|
||||
@@ -72,11 +72,11 @@ function pointerEvent(
|
||||
} as never;
|
||||
}
|
||||
|
||||
function controller() {
|
||||
function canvas() {
|
||||
return {
|
||||
selectNode: vi.fn(),
|
||||
updateNodeTransform: vi.fn(),
|
||||
} as unknown as UiEditorPageController;
|
||||
} as Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
|
||||
}
|
||||
|
||||
function renderInteraction({
|
||||
@@ -88,13 +88,13 @@ function renderInteraction({
|
||||
scale?: number;
|
||||
tree?: UITree | null;
|
||||
} = {}) {
|
||||
const pageController = controller();
|
||||
const canvasProjection = canvas();
|
||||
const viewportRef = { current: { scale } };
|
||||
const hook = renderHook(
|
||||
({ imageId, currentTree }) =>
|
||||
useNodeTransformInteraction({
|
||||
activeImageId: imageId,
|
||||
controller: pageController,
|
||||
canvas: canvasProjection,
|
||||
logicalSize: { width: 320, height: 180 },
|
||||
spaceHeld: false,
|
||||
tree: currentTree,
|
||||
@@ -102,12 +102,12 @@ function renderInteraction({
|
||||
}),
|
||||
{ initialProps: { imageId: activeImageId, currentTree: tree } },
|
||||
);
|
||||
return { ...hook, pageController, viewportRef };
|
||||
return { ...hook, canvasProjection, viewportRef };
|
||||
}
|
||||
|
||||
describe('useNodeTransformInteraction', () => {
|
||||
it('makes drag and resize mutually exclusive, then permits the next gesture', () => {
|
||||
const { result, pageController } = renderInteraction();
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const dragTarget = gestureTarget();
|
||||
const resizeTarget = gestureTarget();
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('useNodeTransformInteraction', () => {
|
||||
});
|
||||
|
||||
expect(resizeTarget.setPointerCapture).not.toHaveBeenCalled();
|
||||
expect(pageController.updateNodeTransform).toHaveBeenCalledTimes(1);
|
||||
expect(canvasProjection.updateNodeTransform).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerUp(pointerEvent(dragTarget, 1, 12, 8));
|
||||
@@ -139,7 +139,7 @@ describe('useNodeTransformInteraction', () => {
|
||||
});
|
||||
|
||||
it('cancels a gesture when its tree changes and ignores its later events', () => {
|
||||
const { result, rerender, pageController } = renderInteraction();
|
||||
const { result, rerender, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
@@ -156,11 +156,11 @@ describe('useNodeTransformInteraction', () => {
|
||||
});
|
||||
|
||||
expect(target.releasePointerCapture).toHaveBeenCalledWith(1);
|
||||
expect(pageController.updateNodeTransform).not.toHaveBeenCalled();
|
||||
expect(canvasProjection.updateNodeTransform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels instead of persisting a non-finite transform', () => {
|
||||
const { result, pageController, viewportRef } = renderInteraction();
|
||||
const { result, canvasProjection, viewportRef } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
@@ -170,7 +170,7 @@ describe('useNodeTransformInteraction', () => {
|
||||
});
|
||||
|
||||
expect(target.releasePointerCapture).toHaveBeenCalledWith(1);
|
||||
expect(pageController.updateNodeTransform).not.toHaveBeenCalled();
|
||||
expect(canvasProjection.updateNodeTransform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('releases a captured pointer when the preview unmounts', () => {
|
||||
|
||||
@@ -68,20 +68,39 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@genarrative/image-canvas-core': resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
'@genarrative/image-canvas-react/styles.css': resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-react/src/styles.css',
|
||||
),
|
||||
'@genarrative/image-canvas-react': resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
alias: [
|
||||
// v1.35.0 publishes this bundle but its package metadata points Vite at
|
||||
// an incomplete source entrypoint. Resolve only the bare module to the
|
||||
// published ESM artifact; its stylesheet remains a valid subpath import.
|
||||
{
|
||||
find: /^@cubone\/react-file-manager$/,
|
||||
replacement: resolve(
|
||||
appRoot,
|
||||
'node_modules/@cubone/react-file-manager/dist/react-file-manager.es.js',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-core',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-react/styles.css',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-react/src/styles.css',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-react',
|
||||
replacement: resolve(
|
||||
repoRoot,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
],
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
server: {
|
||||
|
||||
+31
-15
@@ -553,21 +553,37 @@ export default defineConfig(({ mode }) => {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
'@genarrative/image-canvas-core': path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
'@genarrative/image-canvas-react/styles.css': path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/styles.css',
|
||||
),
|
||||
'@genarrative/image-canvas-react': path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
find: /^@cubone\/react-file-manager$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/node_modules/@cubone/react-file-manager/dist/react-file-manager.es.js',
|
||||
),
|
||||
},
|
||||
{ find: '@', replacement: path.resolve(__dirname, '.') },
|
||||
{
|
||||
find: '@genarrative/image-canvas-core',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-react/styles.css',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/styles.css',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-react',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
],
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
optimizeDeps: {
|
||||
|
||||
+46
-21
@@ -4,32 +4,57 @@ import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@cubone\/react-file-manager$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/node_modules/@cubone/react-file-manager/dist/react-file-manager.es.js',
|
||||
),
|
||||
},
|
||||
// Keep shared components' application-root imports resolvable in tests.
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
'@genarrative/image-canvas-core': path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
'@genarrative/image-canvas-react': path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
{ find: '@', replacement: path.resolve(__dirname, '.') },
|
||||
{
|
||||
find: '@genarrative/image-canvas-core',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-core/src/index.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@genarrative/image-canvas-react',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'packages/image-canvas-react/src/index.ts',
|
||||
),
|
||||
},
|
||||
// The standalone AI shell owns its Tauri guest dependencies. Root tests
|
||||
// use inert aliases so the root H5 package keeps the HostBridge boundary.
|
||||
'@tauri-apps/plugin-clipboard-manager': path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts',
|
||||
),
|
||||
'@tauri-apps/plugin-opener': path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/tests/tauri-plugin-opener.mock.ts',
|
||||
),
|
||||
{
|
||||
find: '@tauri-apps/plugin-clipboard-manager',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/tests/tauri-plugin-clipboard-manager.mock.ts',
|
||||
),
|
||||
},
|
||||
{
|
||||
find: '@tauri-apps/plugin-opener',
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
'apps/ai-game-creator-shell/tests/tauri-plugin-opener.mock.ts',
|
||||
),
|
||||
},
|
||||
// The AI shell has its own install for standalone Tauri builds. Keep root
|
||||
// Vitest on a single React renderer even after that scoped install exists.
|
||||
react: path.resolve(__dirname, 'node_modules/react'),
|
||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
|
||||
},
|
||||
{
|
||||
find: 'react',
|
||||
replacement: path.resolve(__dirname, 'node_modules/react'),
|
||||
},
|
||||
{
|
||||
find: 'react-dom',
|
||||
replacement: path.resolve(__dirname, 'node_modules/react-dom'),
|
||||
},
|
||||
],
|
||||
dedupe: ['react', 'react-dom', 'zustand'],
|
||||
},
|
||||
test: {
|
||||
|
||||
Reference in New Issue
Block a user