修复 AI 游戏创作壳测试解析
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Failing after 2m1s
Project CI / Native shell tests (pull_request) Failing after 2m40s

显式解析文件管理器已发布的 ESM 入口

迁移 UI 编辑器测试到会话投影与几何模块

修正节点交互测试的画布接口契约
This commit is contained in:
2026-08-19 17:18:20 +08:00
parent c5ecc48598
commit 10d5bcf713
6 changed files with 330 additions and 268 deletions
@@ -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', () => {
+33 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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: {