Files
Genarrative/apps/ai-game-creator-shell/tests/runAutoLoadOnEnter.test.tsx
T
suzmii 9eb537c6ff 运行视图没有画面时自动补一次载入(进入项目/切项目的运行页空态)
- index.tsx:新增 autoRunPreviewProjectRef 记账,站在运行视图却没有可嵌入预览且可运行时自动补发一次 onPlay,同一个项目只补一次
- index.tsx:showRunView 先记账再发播放,避免点页签时被自动补发重复触发一次
- 用例:新增 runAutoLoadOnEnter(切项目自动补发一次、同项目不重复、点页签只发一次、有活体预览时不补发)
- 文档:PRD 顶栏条款、验收用例 S17 判据、decision-log 同步自动载入口径
2026-10-04 15:56:41 +08:00

183 lines
5.5 KiB
TypeScript

/** @vitest-environment jsdom */
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
createGameCreationAppManifest,
type GameCreationAppPreviewState,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import ProjectDevelopmentView from '../src/view/project-development';
import { installTauriRuntime } from './tauriRuntimeFake';
/**
* 「进入项目时已经站在运行页,却没有画面」要自动载入一次。
*
* 现场:从另一个项目切过来时工作台不重挂(`mode` 是工作台自己的 state),上一条项目留下的运行
* 视图会原样保留,画面却已经没了;用户看到的就是「客户端运行画面尚未载入 + 点击顶部「运行」
* 页签」,像坏了一样。
*/
const OFFLINE_PREVIEW: GameCreationAppPreviewState = {
status: 'running',
url: 'https://example.com/game',
port: 443,
};
/** 有完成的代码原型才算「可运行」——运行页签才会真的发播放请求。 */
function buildManifest(id: string, name: string) {
const manifest = createGameCreationAppManifest(id, name);
manifest.tasks = manifest.tasks.map((task) =>
task.id === 'code-prototype' ? { ...task, status: 'completed' } : task,
);
return manifest;
}
function installInvoke() {
installTauriRuntime({
core: {
invoke: vi.fn(async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_graph') {
return {
resourceIds: [],
referenceEdges: [],
taskFlows: [],
categories: [],
diagnostics: [],
};
}
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: args?.expectedProjectId,
mode: args?.mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
throw new Error(`unexpected invoke ${command}`);
}),
},
});
}
function renderWorkbench({
projectPath,
name,
preview = OFFLINE_PREVIEW,
onPlay,
}: {
projectPath: string;
name: string;
preview?: GameCreationAppPreviewState;
onPlay: () => void;
}) {
installInvoke();
return render(
<ProjectDevelopmentView
projectName={name}
projectPath={projectPath}
manifest={buildManifest('run-auto-load', name)}
attachments={[]}
recentRunStatus="completed"
recentRunStopReason={null}
preview={preview}
chat={null}
onHomeOpen={vi.fn()}
onProjectsOpen={vi.fn()}
onNotice={vi.fn()}
onPlay={onPlay}
/>,
);
}
afterEach(() => {
cleanup();
vi.clearAllMocks();
vi.restoreAllMocks();
});
describe('运行视图没有画面时的自动载入', () => {
it('切到另一个项目时自动补一次播放,同一个项目不重复发', async () => {
const onPlay = vi.fn();
const view = renderWorkbench({
projectPath: '/tmp/project-a',
name: '项目 A',
onPlay,
});
// 先在「项目 A」上把视图切到运行(没有可嵌入的预览:切过去只有空态)。
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
await screen.findByText('客户端运行画面尚未载入');
expect(onPlay).toHaveBeenCalledTimes(1);
// 工作台不重挂,只换项目:运行视图原样留下,画面是空的 → 自动补一次。
view.rerender(
<ProjectDevelopmentView
projectName="项目 B"
projectPath="/tmp/project-b"
manifest={buildManifest('run-auto-load-b', '项目 B')}
attachments={[]}
recentRunStatus={null}
recentRunStopReason={null}
preview={OFFLINE_PREVIEW}
chat={null}
onHomeOpen={vi.fn()}
onProjectsOpen={vi.fn()}
onNotice={vi.fn()}
onPlay={onPlay}
/>,
);
expect(onPlay).toHaveBeenCalledTimes(2);
// 同一个项目再渲染一次(manifest 刷新、对话更新之类)不该反复发播放请求。
view.rerender(
<ProjectDevelopmentView
projectName="项目 B"
projectPath="/tmp/project-b"
manifest={buildManifest('run-auto-load-b', '项目 B 更新')}
attachments={[]}
recentRunStatus={null}
recentRunStopReason={null}
preview={OFFLINE_PREVIEW}
chat={null}
onHomeOpen={vi.fn()}
onProjectsOpen={vi.fn()}
onNotice={vi.fn()}
onPlay={onPlay}
/>,
);
expect(onPlay).toHaveBeenCalledTimes(2);
});
it('点「运行」页签本身只发一次播放,不被自动载入重复触发', async () => {
const onPlay = vi.fn();
renderWorkbench({
projectPath: '/tmp/project-a',
name: '项目 A',
onPlay,
});
fireEvent.click(screen.getByRole('tab', { name: '运行' }));
expect(onPlay).toHaveBeenCalledTimes(1);
});
it('有活体预览时不补发:画面已经在,没必要再启动一次', async () => {
const onPlay = vi.fn();
renderWorkbench({
projectPath: '/tmp/project-a',
name: '项目 A',
preview: {
status: 'running',
url: 'http://127.0.0.1:4173/',
port: 4173,
},
onPlay,
});
// 会话内确认过的预览会自动切到运行视图;此时画面在,谁都不该发播放请求。
await screen.findByTitle('项目 A 游戏运行画面');
expect(onPlay).not.toHaveBeenCalled();
});
});