/** @vitest-environment jsdom */ /** * 浮层提示的外壳级接线:`ProjectChat` 发一条运行提示,工作台壳真的把它渲染成浮层; * 原来那一行状态栏(进行中进度、失败结论、取消回显)也走同一条通道,页面上不留那一行。 * * 单测(`launcherNoticeToast`)只证明组件本身,`previewActivation` 只证明 App 会调这条通道; * 从「通道被调用」到「用户看到 toast」之间还差一层壳的接线(prop 名、handler、portal 挂点、 * tone 传递)。这里用替身聊天把这一层单独钉住:以后有人把 `` 挪进条件 * 分支、或在传 prop 时写错名字,这条会红。 */ import { act, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { ProjectChatComponentProps } from '../src/features/app-shell/model'; import { WorkspaceLauncherShell } from '../src/features/app-shell/WorkspaceLauncher'; import { createProjectChatRuntimeHarness, pickProjectFromLauncher, testAuthUser, } from './appSurface/harness'; const PROJECT_PATH = '/tmp/run-notice-shell-project'; function StubProjectChat() { return null; } function StubRunNoticeChat({ onRunNotice }: ProjectChatComponentProps) { return (
); } /** * 打开工作台所需的最小命令集照抄 `home.suite` 的活动清单用例:项目目录探测 + 清单 + * 资源图 / 布局读回,其余命令沿用聊天 harness。 */ function renderWorkbench() { const manifest = createGameCreationAppManifest( 'run-notice-shell-project', '运行提示接线项目', ); const runtimeHarness = createProjectChatRuntimeHarness({ projectPath: PROJECT_PATH, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'get_design_agent_runtime_mode') return null; if (command === 'inspect_local_project_directory') { return { projectPath: PROJECT_PATH, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'read_local_project_resource_graph') { return { resourceIds: [], referenceEdges: [], taskFlows: [], connectionIndex: [], producerAssignments: [], dependencyDepths: [], unresolvedReferenceResourceIds: [], cyclicResourceIds: [], cyclicTaskIds: [], producerMappingTruncated: false, }; } if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId: manifest.projectId, mode: args?.mode, revision: 0, positions: [], updatedAt: 0, }; } return runtimeHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke: invoke as never }, event: { listen: runtimeHarness.listen as never }, }; render( , ); pickProjectFromLauncher(PROJECT_PATH); } function toastElement() { return document.querySelector('[data-launcher-notice-toast="true"]'); } describe('浮层提示的外壳接线', () => { it('成功提示渲染成浮层,且不落在对话容器里', async () => { renderWorkbench(); await screen.findByLabelText('项目开发工作台'); fireEvent.click( await screen.findByRole('button', { name: '触发成功提示' }), ); await waitFor(() => expect(toastElement()).not.toBeNull()); expect(toastElement()?.textContent ?? '').toContain( '运行通过,已载入客户端运行视图', ); // 「对话区里没有这条提示」由 `previewActivation` 用真实聊天容器断言;这里用的替身 // 聊天没有消息列表,重复断言只会自证。 }); it('失败提示按 alert 渲染', async () => { renderWorkbench(); await screen.findByLabelText('项目开发工作台'); fireEvent.click( await screen.findByRole('button', { name: '触发失败提示' }), ); await waitFor(() => expect(toastElement()?.querySelector('[role="alert"]')).not.toBeNull(), ); expect(toastElement()?.textContent ?? '').toContain( '运行游戏失败:预览端口被占用', ); }); it('状态栏退役:进行中状态与取消回显都只走浮层,首页不留那一行', async () => { let finishDirectoryPick: ((path: string | null) => void) | null = null; const directoryPick = new Promise((resolve) => { finishDirectoryPick = resolve; }); const runtimeHarness = createProjectChatRuntimeHarness({}); const invoke = vi.fn(async (command: string) => { if (command === 'pick_local_project_directory') { return directoryPick; } return runtimeHarness.invoke(command); }); window.__TAURI__ = { core: { invoke: invoke as never }, event: { listen: runtimeHarness.listen as never }, }; render( , ); fireEvent.click(await screen.findByRole('button', { name: '打开项目' })); // 进行中状态:粘性浮层,页面上(以前那一行的位置)不留文字。 await waitFor(() => expect(toastElement()?.textContent ?? '').toContain('正在选择项目'), ); expect(screen.getByLabelText('首页输入').textContent ?? '').not.toContain( '正在选择项目', ); // 取消:粘性状态被一次性回显替换掉,同样只在浮层里。 await act(async () => { finishDirectoryPick?.(null); await directoryPick; }); await waitFor(() => expect(toastElement()?.textContent ?? '').toContain('已取消'), ); expect(screen.getByLabelText('首页输入').textContent ?? '').not.toContain( '已取消', ); }); it('粘性状态只长在首页 / 项目组,进项目工作台不再飘在界面上', async () => { renderWorkbench(); await screen.findByLabelText('项目开发工作台'); // 「打开项目」把状态写成「已打开项目」后立刻切到工作台:那一行原来只长在列表页上, // 换成浮层也不能飘到工作台上来。 expect(toastElement()).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '首页' })); await waitFor(() => expect(toastElement()?.textContent ?? '').toContain('已打开项目'), ); }); });