Files
Genarrative/apps/ai-game-creator-shell/tests/resourceJsonCanvas.test.tsx
T
k88936 25c5ecdf6e
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m29s
Project CI / Backend tests (pull_request) Failing after 4m38s
Project CI / Frontend tests (pull_request) Successful in 3m33s
Project CI / Repository checks (pull_request) Failing after 4m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Successful in 6m35s
测试:全仓测试统一 Tauri 运行时替身安装点
- 78 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,覆盖 appSurface 之外的 37 个测试文件
- 31 处 delete window.__TAURI__ / window.__TAURI__ = undefined 复位改为 resetTauriRuntime
- 删除 recentProjectsHook、projectCreationDirectory、projectResourceLiveIntegration、useProjectResourceCardPreviews、resourceVersionReplacementModel、unityProjectOpen 里的本地 asTauriInvoke / asTauriListen 适配器与相关类型别名
- designProjectRestore 的 setup 参数由 ReturnType<typeof vi.fn> 收窄为具体 invoke 签名
- 迁移后仅 tests/tauriRuntimeFake.ts 直接读写 window.__TAURI__
2026-10-03 20:19:45 +08:00

219 lines
7.5 KiB
TypeScript

/** @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from 'vitest';
import type {
GameCreationAppAssetKind,
GameCreationAppAssetManifestEntry,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
createGameCreationAppManifest,
fireEvent,
installResizeObserverStub,
ProjectDevelopmentView,
React,
render,
screen,
waitFor,
within,
} from './appSurface/harness';
import { installTauriRuntime, resetTauriRuntime } from './tauriRuntimeFake';
vi.mock('@tauri-apps/api/core', async () => ({
...(await vi.importActual<typeof import('@tauri-apps/api/core')>(
'@tauri-apps/api/core',
)),
invoke: (command: string, args?: Record<string, unknown>) =>
window.__TAURI__!.core!.invoke!(command, args),
}));
const projectId = 'json-canvas';
const projectPath = '/tmp/json-canvas';
const emptyState = {
ui_trees: [],
ui_design_images: {},
sprite_assets: {},
font_assets: {},
};
async function mountJsonCanvas(
kind: GameCreationAppAssetKind = 'ui-design',
rejectPreview = false,
) {
installResizeObserverStub();
const manifest = createGameCreationAppManifest(projectId, 'JSON 资源测试');
const baseAssets: Array<
Pick<GameCreationAppAssetManifestEntry, 'id' | 'kind' | 'localPath'>
> = [
{ id: 'design', kind, localPath: 'ui/design.json' },
{ id: 'ordinary', kind: 'unknown', localPath: 'assets/ordinary.json' },
{ id: 'spoof', kind: 'unknown', localPath: 'ui/spoof.json' },
];
manifest.assets = baseAssets.map((asset) => ({
...asset,
mediaType: 'application/json',
category: 'document' as const,
source: { kind: 'generated' as const },
}));
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_graph') {
const ids = (args?.resources as Array<{ resourceId: string }>).map(
(item) => item.resourceId,
);
return {
resourceIds: ids,
referenceEdges: [],
taskFlows: [],
producerAssignments: [],
dependencyDepths: ids.map((resourceId) => ({
resourceId,
dependencyDepth: 0,
})),
connectionIndex: ids.map((resourceId) => ({
resourceId,
upstreamReferenceResourceIds: [],
downstreamReferenceResourceIds: [],
referenceEdgeIds: [],
taskFlowIds: [],
})),
unresolvedReferenceResourceIds: [],
cyclicResourceIds: [],
cyclicTaskIds: [],
producerMappingTruncated: false,
};
}
if (
command === 'read_local_project_resource_canvas_layout' ||
command === 'update_local_project_resource_canvas_layout'
) {
const layout = {
schemaVersion: 'game-creator-resource-layout.v1',
projectId,
mode: args?.mode,
revision: Number(args?.expectedRevision ?? 0) + 1,
positions: args?.positions ?? [],
updatedAt: 1,
};
return command.startsWith('update_')
? { status: 'updated', layout }
: layout;
}
if (command === 'read_local_project_text_preview') {
if (rejectPreview) throw new Error('文档读取失败');
const path = args?.relativePath;
return {
path,
mediaType: 'application/json',
byteLen: 20,
content:
path === 'assets/ordinary.json'
? '{"text":"# 不应当作标题"}'
: JSON.stringify({
schemaVersion: 'game-creator-ui-design-state.v1',
state: emptyState,
}),
...(path === 'ui/design.json' ? { uiDesignAssetId: 'design' } : {}),
};
}
if (command === 'load_ui_design_state')
return { revision: 0, state: emptyState };
if (
command === 'list_pending_local_project_resource_edits' ||
command === 'list_local_project_asset_generations'
)
return [];
throw new Error(`unexpected command ${command}`);
},
);
installTauriRuntime({ core: { invoke } });
const onManifestChange = vi.fn();
render(
<ProjectDevelopmentView
projectPath={projectPath}
projectName={manifest.name}
manifest={manifest}
attachments={[]}
recentRunStatus={null}
recentRunStopReason={null}
chat={<div>对话</div>}
onHomeOpen={() => {}}
onProjectsOpen={() => {}}
onManifestChange={onManifestChange}
/>,
);
fireEvent.click(await screen.findByRole('button', { name: '打开文档' }));
const card = (id: string) =>
document.querySelector<HTMLElement>(
`.game-resource-book-scene-card.is-expanded [data-resource-card-id="asset:${id}"]`,
)!;
await waitFor(() =>
expect(card('design')?.getAttribute('data-preview-status')).toBe(
rejectPreview ? 'failed' : 'loaded',
),
);
return { card, invoke, onManifestChange };
}
afterEach(() => {
resetTauriRuntime();
});
describe('JSON 画布卡片与入口', () => {
it.each(['ui-design', 'document'] as const)(
'合法 State 的 %s 登记显示 UI 设计,进入现有编辑器且不修改 manifest',
async (kind) => {
const { card, invoke, onManifestChange } = await mountJsonCanvas(kind);
const design = card('design');
expect(design.getAttribute('data-json-presentation')).toBe('ui-design');
expect(within(design).getByText('UI 设计')).not.toBeNull();
expect(within(design).getByText('UI 编辑器')).not.toBeNull();
expect(design.textContent).not.toContain('schemaVersion');
fireEvent.click(design.querySelector('button')!);
fireEvent.click(await screen.findByRole('button', { name: 'UI 编辑器' }));
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('load_ui_design_state', {
input: {
projectPath,
expectedProjectId: projectId,
assetId: 'design',
},
}),
);
expect(onManifestChange).not.toHaveBeenCalled();
},
);
it('普通 JSON 和伪 UI 文本保持 JSON 展示,无编辑器入口,详情按代码渲染', async () => {
const { card } = await mountJsonCanvas();
for (const id of ['ordinary', 'spoof']) {
const element = card(id);
expect(element.getAttribute('data-json-presentation')).toBe('json');
expect(
element.querySelector('[data-resource-type="JSON"]'),
).not.toBeNull();
expect(element.textContent).not.toContain('schemaVersion');
fireEvent.click(element.querySelector('button')!);
expect(screen.queryByRole('button', { name: 'UI 编辑器' })).toBeNull();
}
fireEvent.click(card('ordinary').querySelector('button')!);
fireEvent.click(await screen.findByRole('button', { name: '预览' }));
const dialog = await screen.findByRole('dialog', { name: '文档预览' });
await waitFor(() =>
expect(dialog.querySelector('pre code')?.textContent).toContain(
'{"text":"# 不应当作标题"}',
),
);
expect(
within(dialog).queryByRole('heading', { name: '不应当作标题' }),
).toBeNull();
});
it('JSON 读取失败不授予编辑入口,仍可查看明确的读取错误', async () => {
const { card } = await mountJsonCanvas('unknown', true);
fireEvent.click(card('design').querySelector('button')!);
expect(screen.queryByRole('button', { name: 'UI 编辑器' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '预览' }));
expect(await screen.findByRole('alert')).not.toBeNull();
});
});