diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index 4e2f41816..379024e85 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -11,12 +11,6 @@ import { } from './styleCascade'; const STYLES_PATH = repoPath('apps/ai-game-creator-shell/src/styles.css'); -const CHAT_VIEW_PATH = repoPath( - 'apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx', -); -const CHAT_CONVERSATION_PATH = repoPath( - 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx', -); const CHAT_COMPOSER_PATH = repoPath( 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx', ); @@ -591,62 +585,6 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文 expect(pixelValue(topbar, 'min-height')).toBe(44); }); - it('输入盒仍落在 form.project-chat-composer 子树里,交互控件原位', () => { - const viewSource = readFileSync(CHAT_VIEW_PATH, 'utf8'); - const conversationSource = readFileSync(CHAT_CONVERSATION_PATH, 'utf8'); - const composerSource = readFileSync(CHAT_COMPOSER_PATH, 'utf8'); - - // 面板三段式的顺序:顶栏 → 会话区 → 输入盒,组合顺序由视图持有。 - const conversationOpen = viewSource.indexOf( - '
', - ); - expect(conversationOpen).toBeGreaterThanOrEqual(0); - const headerIndex = viewSource.indexOf( - ''); - expect(formEnd, '输入盒(form)没有闭合').toBeGreaterThan(formOpen); - const formSource = composerSource.slice( - formOpen, - formEnd + ''.length, - ); - - // 编辑器、`+` / `@` 引用、模型选择、提交按钮全在这棵子树里,位置只由样式挪。 - expect(formSource).toContain('ResourceReferenceInput'); - // 输入盒里不再有「项目名 · 本地」信息标签行(按需求删除)。 - expect(formSource).not.toContain('project-chat-composer-context'); - expect(formSource).toContain('project-chat-composer-controls'); - expect(formSource).toContain('project-chat-reference-trigger'); - expect(formSource).toContain('ConversationModelSelect'); - expect(formSource).toContain('onSubmit={'); - // 提交按钮本体还是那只按钮,只是以变量形式引用进这棵子树。 - expect(composerSource).toContain('className="project-chat-submit-button"'); - }); - /** * 面板纵向必须撑满 `.game-workbench-chat` 的高度。 * diff --git a/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx b/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx deleted file mode 100644 index ed29f627d..000000000 --- a/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx +++ /dev/null @@ -1,359 +0,0 @@ -// @vitest-environment jsdom -/** - * 客户端「发布」入口的可见反馈。 - * - * 陶泥儿 tab 左半是内联的发布资料表单,内容末尾的橙色「打包并发布」一次点击先构建打包、 - * 再提交发布。打包用独立的全屏进度弹窗(失败留在弹窗内),成功后不再弹出发布面板。 - */ -import { readFileSync } from 'node:fs'; - -import { beforeEach, describe, it, vi } from 'vitest'; - -import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; -import type { ProjectPermissionPolicyView } from '../src/app/types'; -import { - App, - createGameCreationAppManifest, - createProjectChatRuntimeHarness, - emptyProjectPolicy, - expect, - fireEvent, - render, - screen, - waitFor, - within, -} from './appSurface/harness'; -import { repoPath } from './repoPath'; -import { - declaration, - parseStyleSheet, - resolveDeclarations, -} from './styleCascade'; -import { installTauriRuntime } from './tauriRuntimeFake'; - -vi.mock('../src/services/gameDistributionPublish', async (importOriginal) => { - const actual = - await importOriginal< - typeof import('../src/services/gameDistributionPublish') - >(); - return { - ...actual, - // 面板打开就会回读发布绑定;这里按「首次发布」放行,避免落到真实原生命令。 - readGameDistributionPublication: vi.fn(async () => ({ - state: 'first-publish', - binding: null, - refreshed: true, - draft: null, - visibility: null, - })), - }; -}); - -// 表单只负责选图并调用上传;这里替换掉真实直传,让内联表单能在测试里拿到封面 ID。 -vi.mock('../src/services/assetDirectUpload', () => ({ - uploadPlatformMediaAsset: vi.fn(async () => ({ - assetObjectId: 'asset_cover', - objectKey: 'game-distribution/cover/cover.png', - })), -})); - -const PROJECT_PATH = '/tmp/game-publish-feedback-project'; -const PROJECT_ID = 'game-publish-feedback-project'; - -function withPrototypeStatus( - manifest: GameCreationAppManifest, - status: 'pending' | 'completed', -): GameCreationAppManifest { - return { - ...manifest, - tasks: manifest.tasks.map((task) => - task.id === 'code-prototype' ? { ...task, status } : task, - ), - }; -} - -function createFixtureManifest(): GameCreationAppManifest { - return withPrototypeStatus( - createGameCreationAppManifest(PROJECT_ID, '发布反馈项目'), - 'completed', - ); -} - -function createPendingPrototypeManifest(): GameCreationAppManifest { - return withPrototypeStatus( - createGameCreationAppManifest(PROJECT_ID, '原型未完成项目'), - 'pending', - ); -} - -function installTauri( - options: { - exportPackage?: () => unknown; - manifest?: GameCreationAppManifest; - policy?: ProjectPermissionPolicyView; - readPolicy?: () => unknown; - } = {}, -) { - const manifest = options.manifest ?? createFixtureManifest(); - const chatHarness = createProjectChatRuntimeHarness({ - projectPath: PROJECT_PATH, - }); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'get_local_game_manifest') return manifest; - if (command === 'get_local_game_project_revision') return { revision: 1 }; - if (command === 'read_project_permission_policy') { - if (options.readPolicy) return options.readPolicy(); - return options.policy ?? emptyProjectPolicy(); - } - if (command === 'export_local_project_package') { - if (options.exportPackage) return options.exportPackage(); - return { - projectPath: PROJECT_PATH, - packagePath: `${PROJECT_PATH}/exports/game.zip`, - packageRelativePath: 'exports/game.zip', - fileCount: 1, - totalBytes: 1, - }; - } - return chatHarness.invoke(command, args); - }, - ); - installTauriRuntime({ - core: { invoke }, - event: { listen: chatHarness.listen }, - }); - return { invoke }; -} - -function renderPublishProject( - manifest: GameCreationAppManifest = createFixtureManifest(), -) { - return render( - , - ); -} - -/** - * 发布入口在导出产物面板的陶泥儿 tab:先开导出面板,再进陶泥儿,等灰度读到后才可点。 - */ -async function openTaonierPublishEntry() { - fireEvent.click(await screen.findByRole('button', { name: '导出产物' })); - fireEvent.click(await screen.findByRole('button', { name: '陶泥儿' })); - const publish = await screen.findByRole('button', { name: '打包并发布' }); - await waitFor(() => expect(publish).toHaveProperty('disabled', false)); - return publish; -} - -/** 内联表单只有拿到封面素材 ID 才算校验通过;用假上传把封面补齐后再点主行动。 */ -async function attachCover() { - fireEvent.change(await screen.findByLabelText('游戏封面图片'), { - target: { - files: [new File(['cover-bytes'], 'cover.png', { type: 'image/png' })], - }, - }); - await screen.findByText(/已选择「cover\.png」/u); -} - -beforeEach(() => { - window.history.pushState({}, '', '/'); -}); - -function createExportPackageResult() { - return { - projectPath: PROJECT_PATH, - packagePath: `${PROJECT_PATH}/exports/game.zip`, - packageRelativePath: 'exports/game.zip', - fileCount: 1, - totalBytes: 1, - }; -} - -function createConfirmPolicy() { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.export_package'], - }, - }; -} - -function createDeferred() { - let resolve!: (value: T) => void; - let reject!: (reason?: unknown) => void; - const promise = new Promise((resolvePromise, rejectPromise) => { - resolve = resolvePromise; - reject = rejectPromise; - }); - return { promise, resolve, reject }; -} - -describe('客户端发布入口的可见反馈', () => { - it('首个可运行原型未完成时显示独立阻断弹窗,不触发用户项目构建', async () => { - const exportPackage = vi.fn(createExportPackageResult); - const manifest = createPendingPrototypeManifest(); - installTauri({ exportPackage, manifest }); - - renderPublishProject(manifest); - - const publish = await openTaonierPublishEntry(); - // 表单先过本地门禁,原型阻断才会接管;先补齐封面让校验通过。 - await attachCover(); - fireEvent.click(publish); - const surface = await screen.findByLabelText('游戏发布对话栏'); - const noticeDialog = await screen.findByRole('dialog', { - name: '发布提示', - }); - expect(noticeDialog.textContent ?? '').toContain( - '首个可运行原型尚未完成,暂不能发布;请先完成可运行原型并通过运行验证。', - ); - expect(surface.textContent ?? '').not.toContain( - '首个可运行原型尚未完成,暂不能发布', - ); - expect(surface.textContent ?? '').not.toContain('正在检查发布权限…'); - expect(exportPackage).not.toHaveBeenCalled(); - - fireEvent.click(within(noticeDialog).getByRole('button', { name: '关闭' })); - await waitFor(() => { - expect(screen.queryByRole('dialog', { name: '发布提示' })).toBeNull(); - }); - }); - - it('打包时显示全屏进度遮罩,成功后直接进入发布提交(不再弹发布面板)', async () => { - const deferred = - createDeferred>(); - const exportPackage = vi.fn(() => deferred.promise); - installTauri({ exportPackage }); - renderPublishProject(); - - const publish = await openTaonierPublishEntry(); - await attachCover(); - fireEvent.click(publish); - - const progressDialog = await screen.findByRole('dialog', { - name: '发布进度', - }); - expect(progressDialog.textContent ?? '').toContain( - '正在构建并打包试玩包,请稍候…', - ); - expect(progressDialog.textContent ?? '').toContain( - '发布完成前请保持客户端开启,页面暂时不可操作。', - ); - expect( - progressDialog.closest('.game-publish-progress-overlay'), - ).not.toBeNull(); - expect(exportPackage).toHaveBeenCalledTimes(1); - expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull(); - - deferred.resolve(createExportPackageResult()); - await waitFor(() => { - expect(screen.queryByRole('dialog', { name: '发布进度' })).toBeNull(); - }); - // 校验通过才会走到打包;打包成功后直接提交发布,不再弹旧的发布面板。 - expect(screen.queryByRole('dialog', { name: '发布到游戏广场' })).toBeNull(); - }); - - it('打包失败时在进度弹窗里显示错误并允许关闭', async () => { - installTauri({ - exportPackage: () => { - throw new Error('构建可玩版本失败:缺少入口'); - }, - }); - renderPublishProject(); - - const publish = await openTaonierPublishEntry(); - await attachCover(); - fireEvent.click(publish); - - const failureDialog = await screen.findByRole('dialog', { - name: '发布失败', - }); - expect(failureDialog.textContent ?? '').toContain( - '构建可玩版本失败:缺少入口', - ); - fireEvent.click( - within(failureDialog).getByRole('button', { name: '关闭' }), - ); - await waitFor(() => { - expect(screen.queryByRole('dialog', { name: '发布失败' })).toBeNull(); - }); - }); - - it('发布进度遮罩固定覆盖整个工作区并压暗背景', () => { - const rules = parseStyleSheet( - readFileSync( - repoPath('apps/ai-game-creator-shell/src/styles.css'), - 'utf8', - ), - ); - const overlay = resolveDeclarations( - rules, - ['.game-publish-progress-overlay'], - 1440, - ); - expect(declaration(overlay, 'position')).toBe('fixed'); - // 遮罩从自绘标题栏下方开始:发布进行中仍然要能最小化 / 关闭窗口。 - expect(declaration(overlay, 'top')).toBe('var(--window-chrome-height)'); - expect(declaration(overlay, 'right')).toBe('0'); - expect(declaration(overlay, 'bottom')).toBe('0'); - expect(declaration(overlay, 'left')).toBe('0'); - expect(declaration(overlay, 'z-index')).toBe('500'); - expect(declaration(overlay, 'pointer-events')).toBe('auto'); - expect(declaration(overlay, 'background')).toBe('rgb(35 24 19 / 62%)'); - }); - - it('策略要求确认时不再回到聊天确认卡片,直接进入进度弹窗', async () => { - const exportPackage = vi.fn(createExportPackageResult); - installTauri({ exportPackage, policy: createConfirmPolicy() }); - renderPublishProject(); - - const publish = await openTaonierPublishEntry(); - await attachCover(); - fireEvent.click(publish); - - await waitFor(() => expect(exportPackage).toHaveBeenCalledTimes(1)); - expect(screen.queryByText('project.export_package')).toBeNull(); - expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull(); - }); - - it('项目还没起名时「打包并发布」不变灰,点击后给出可读的必填提示', async () => { - const exportPackage = vi.fn(createExportPackageResult); - const manifest: GameCreationAppManifest = { - ...createFixtureManifest(), - name: '', - goal: '', - }; - installTauri({ exportPackage, manifest }); - renderPublishProject(manifest); - - const publish = await openTaonierPublishEntry(); - expect(publish).toHaveProperty('disabled', false); - - fireEvent.click(publish); - - // 按钮始终可点,校验在前端当场给出、由「发布提示」弹窗承载;不通过时不为一次注定失败的发布白跑构建。 - const noticeDialog = await screen.findByRole('dialog', { - name: '发布提示', - }); - expect(noticeDialog.textContent ?? '').toContain('请先填写游戏名称'); - expect(exportPackage).not.toHaveBeenCalled(); - }); - - it('没选封面点「打包并发布」:直接弹发布提示,不打包也不出进度弹窗', async () => { - const exportPackage = vi.fn(createExportPackageResult); - installTauri({ exportPackage }); - renderPublishProject(); - - fireEvent.click(await openTaonierPublishEntry()); - - const noticeDialog = await screen.findByRole('dialog', { - name: '发布提示', - }); - expect(noticeDialog.textContent ?? '').toContain( - '请先选择游戏封面(JPG/PNG/WebP)', - ); - expect(exportPackage).not.toHaveBeenCalled(); - expect(screen.queryByRole('dialog', { name: '发布进度' })).toBeNull(); - }); -});