Files
Genarrative/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx
T
menghao ad7b8095cd
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 7s
Project CI / Frontend tests (pull_request) Failing after 5m5s
Project CI / Native shell tests (pull_request) Failing after 6m16s
修复素材画布保存区与项目状态展示
调整素材保存区响应式布局,固定资源用途并保留精修继承语义。
收口普通用户项目路径展示,补充画布与工作台回归测试。
同步更新技术方案、产品需求文档与项目长期记忆。
2026-08-06 15:14:43 +08:00

356 lines
12 KiB
TypeScript

/** @vitest-environment jsdom */
import React, { useState } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
const canvasFixture = vi.hoisted(() => ({
manifest: null as GameCreationAppManifest | null,
revision: 0,
sequence: 0,
}));
vi.mock('../src/features/asset-canvas/AssetCanvasSurface', async () => {
const ReactModule = await import('react');
return {
AssetCanvasSurface: (props: {
scope: {
projectId: string;
draftId: string;
intent: 'create' | 'refine';
sourceAssetId: string | null;
};
sessionId: string;
initialAssetName?: string;
initialAssetKind?: string;
onCancel?: () => void;
onSaveAttempt?: (attempt: {
saveAttemptId: string;
sessionId: string;
projectId: string;
draftId: string;
commitId: string;
}) => void;
onCommitted?: (notification: Record<string, unknown>) => void;
}) =>
ReactModule.createElement(
'section',
{ 'aria-label': '测试素材创作画布' },
ReactModule.createElement(
'span',
null,
`${props.scope.intent}:${props.scope.sourceAssetId ?? 'none'}:${props.initialAssetName ?? 'unset'}:${props.initialAssetKind ?? 'unset'}`,
),
ReactModule.createElement(
'button',
{ type: 'button', onClick: props.onCancel },
'取消并返回',
),
ReactModule.createElement(
'button',
{
type: 'button',
onClick: () => {
const base = canvasFixture.manifest;
if (!base) throw new Error('missing manifest fixture');
canvasFixture.sequence += 1;
canvasFixture.revision += 1;
const assetId = `canvas-output-${canvasFixture.sequence}`;
const commitId = `commit-${canvasFixture.sequence}`;
const sourceResourceId = props.scope.sourceAssetId
? `asset:${props.scope.sourceAssetId}`
: null;
const nextManifest = {
...base,
assets: [
...base.assets,
{
id: assetId,
kind: 'art-image',
mediaType: 'image/png',
localPath: `assets/${assetId}.png`,
source: {
kind: 'canvas' as const,
taskId: null,
resourceId: `canvas-resource-${canvasFixture.sequence}`,
referenceResourceIds: sourceResourceId
? [sourceResourceId]
: [],
},
},
],
};
canvasFixture.manifest = nextManifest;
props.onSaveAttempt?.({
saveAttemptId: `save-${canvasFixture.sequence}`,
sessionId: props.sessionId,
projectId: props.scope.projectId,
draftId: props.scope.draftId,
commitId,
});
props.onCommitted?.({
source: 'command',
projectPath: '/tmp/live-canvas-integration',
projectId: props.scope.projectId,
draftId: props.scope.draftId,
commitId,
assetId,
manifest: nextManifest,
projectRevision: canvasFixture.revision,
committedProjectRevision: canvasFixture.revision,
eventId: `event-${canvasFixture.sequence}`,
});
},
},
'完成测试保存',
),
),
};
});
import ProjectDevelopmentView from '../src/view/project-development';
import {
createGameCreationAppManifest,
fireEvent,
render,
screen,
waitFor,
} from './appSurface/harness';
const projectPath = '/tmp/live-canvas-integration';
function graphFor(
resources: Array<{ resourceId: string }>,
manifest: GameCreationAppManifest,
) {
const resourceIds = resources.map((resource) => resource.resourceId);
const referenceEdges = manifest.assets.flatMap((asset) =>
(asset.source.referenceResourceIds ?? []).map((sourceResourceId) => ({
id: `reference:${sourceResourceId}:${asset.id}`,
kind: 'asset-reference' as const,
sourceResourceId,
targetResourceId: `asset:${asset.id}`,
cyclic: false,
})),
);
return {
resourceIds,
referenceEdges,
taskFlows: [],
connectionIndex: resourceIds.map((resourceId) => ({
resourceId,
upstreamReferenceResourceIds: referenceEdges
.filter((edge) => edge.targetResourceId === resourceId)
.map((edge) => edge.sourceResourceId),
downstreamReferenceResourceIds: [],
referenceEdgeIds: referenceEdges
.filter(
(edge) =>
edge.targetResourceId === resourceId ||
edge.sourceResourceId === resourceId,
)
.map((edge) => edge.id),
taskFlowIds: [],
})),
producerAssignments: [],
dependencyDepths: resourceIds.map((resourceId) => ({
resourceId,
dependencyDepth: referenceEdges.some(
(edge) => edge.targetResourceId === resourceId,
)
? 1
: 0,
})),
unresolvedReferenceResourceIds: [],
cyclicResourceIds: [],
cyclicTaskIds: [],
producerMappingTruncated: false,
};
}
function LiveWorkbench() {
const initial = createGameCreationAppManifest(
'live-canvas-project',
'实时画布项目',
);
initial.assets = [
{
id: 'source-art',
kind: 'art-image',
mediaType: 'image/png',
localPath: 'assets/source-art.png',
source: { kind: 'canvas', taskId: null, resourceId: 'source-resource' },
},
];
const [manifest, setManifest] = useState(initial);
canvasFixture.manifest = manifest;
return (
<ProjectDevelopmentView
projectName={manifest.name}
projectPath={projectPath}
manifest={manifest}
attachments={[]}
recentRunStatus={null}
recentRunStopReason={null}
supervisor={<div>Supervisor</div>}
onHomeOpen={() => undefined}
onProjectsOpen={() => undefined}
onManifestChange={(_path, nextManifest) => setManifest(nextManifest)}
/>
);
}
describe('project resource live canvas integration', () => {
afterEach(() => {
delete window.__TAURI__;
canvasFixture.manifest = null;
canvasFixture.revision = 0;
canvasFixture.sequence = 0;
});
function installTauri() {
const layoutWrites: Array<Record<string, unknown>> = [];
const graphReads: Array<Record<string, unknown>> = [];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'get_local_game_project_revision') {
return { revision: canvasFixture.revision };
}
if (command === 'read_local_project_resource_graph') {
graphReads.push(structuredClone(args ?? {}));
return graphFor(
(args?.resources ?? []) as Array<{ resourceId: string }>,
canvasFixture.manifest!,
);
}
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: 'live-canvas-project',
mode: args?.mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
if (command === 'update_local_project_resource_canvas_layout') {
layoutWrites.push(structuredClone(args ?? {}));
return {
status: 'updated',
layout: {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: 'live-canvas-project',
mode: args?.mode,
revision: Number(args?.expectedRevision ?? 0) + 1,
positions: args?.positions,
updatedAt: 1,
},
};
}
if (command === 'read_local_project_image_preview') {
return {
path: String(args?.relativePath ?? ''),
mediaType: 'image/png',
byteLen: 1,
dataUrl: 'data:image/png;base64,AA==',
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: async () => () => undefined },
};
return { graphReads, layoutWrites };
}
it('enters create/refine in the central view, cancels to context, and coordinates both layouts after a durable refine', async () => {
const { graphReads, layoutWrites } = installTauri();
render(<LiveWorkbench />);
const sourceCard = await screen.findByRole('button', {
name: /source-art\.png/,
});
fireEvent.click(sourceCard);
fireEvent.click(screen.getByRole('button', { name: '精修资源' }));
expect(
(await screen.findByLabelText('测试素材创作画布')).textContent,
).toContain('refine:source-art:source-art:art-image');
fireEvent.click(screen.getByRole('button', { name: '取消并返回' }));
expect(
await screen.findByRole('region', { name: /source-art\.png/ }),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '精修资源' }));
fireEvent.click(
await screen.findByRole('button', { name: '完成测试保存' }),
);
expect(
await screen.findByRole('region', { name: /canvas-output-1\.png/ }),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
expect(
await screen.findByText(/canvas-output-1\.png 引用 source-art\.png/),
).not.toBeNull();
await waitFor(() => {
expect(
layoutWrites.some(
(write) =>
write.mode === 'dependency' &&
(write.positions as Array<{ resourceId: string }>).some(
(position) => position.resourceId === 'asset:canvas-output-1',
),
),
).toBe(true);
expect(
layoutWrites.some(
(write) =>
write.mode === 'type' &&
(write.positions as Array<{ resourceId: string }>).some(
(position) => position.resourceId === 'asset:canvas-output-1',
),
),
).toBe(true);
});
expect(
graphReads.some((read) => {
const resourceIds = (
read.resources as Array<{ resourceId: string }>
).map((resource) => resource.resourceId);
return (
resourceIds.includes('asset:source-art') &&
resourceIds.includes('asset:canvas-output-1')
);
}),
).toBe(true);
});
it('keeps an existing search when the new resource is hidden and locates only after the explicit action', async () => {
installTauri();
render(<LiveWorkbench />);
const search = await screen.findByLabelText('搜索项目资源');
fireEvent.change(search, { target: { value: 'source-art' } });
fireEvent.click(screen.getByRole('button', { name: '新增资源' }));
fireEvent.click(
await screen.findByRole('button', { name: '完成测试保存' }),
);
expect(
await screen.findByText('新资源已保存,但被当前搜索条件隐藏'),
).not.toBeNull();
expect((search as HTMLInputElement).value).toBe('source-art');
fireEvent.click(screen.getByRole('button', { name: '清除搜索并定位' }));
expect(
await screen.findByRole('region', { name: /canvas-output-1\.png/ }),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '收起资源' }));
expect(
((await screen.findByLabelText('搜索项目资源')) as HTMLInputElement)
.value,
).toBe('');
});
});