/** @vitest-environment jsdom */ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { ResourceCanvasProjectNamingTask } from '../src/features/resource-canvas/resourceCanvasAssetGenerationTaskModel'; import { ResourceCanvasAssetGenerationTasksPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView'; import { cleanup, render, screen, within } from './appSurface/harness'; /** * 「生成任务」侧栏里的「AI 项目命名」行。 * * 它与素材生成共用同一份账本、同一个事件与同一套卡片样式,但有两条自己的口径: * 1. **展示名固定**为「AI 项目命名」:不渲染素材缩略图 / 提示词 / 派发与「定位到素材」; * 2. **结论只在终态渲染**:进行中由状态徽章表达(排队中 / 命名中),终态才显示后端给的结论 * (已应用 AI 名称 / 已跳过 / 失败保留兜底名)。 */ afterEach(cleanup); function namingTask( overrides: Partial = {}, ): ResourceCanvasProjectNamingTask { return { taskId: 'project-naming:local-project-1', projectId: 'local-project-1', status: 'running', phaseDetail: '正在等待 AI 命名结果。', createdAtMillis: 1000, startedAtMillis: 1200, finishedAtMillis: null, error: null, ...overrides, }; } function renderPanel(tasks: readonly ResourceCanvasProjectNamingTask[]) { render( , ); } describe('生成任务侧栏里的 AI 项目命名行', () => { it('进行中只给状态徽章,不渲染结论、缩略图、提示词或定位动作', () => { renderPanel([namingTask()]); const row = document.querySelector( '[data-task-source="project-naming"]', ) as HTMLElement | null; expect(row).not.toBeNull(); expect(within(row!).getByText('AI 项目命名')).not.toBeNull(); expect(within(row!).getByText('命名中')).not.toBeNull(); // 进行中不显示后端阶段文案;也没有素材任务的缩略图/提示词/派发按钮。 expect(within(row!).queryByText('正在等待 AI 命名结果。')).toBeNull(); expect(within(row!).queryByRole('button', { name: /定位素材/ })).toBeNull(); expect(row!.querySelector('img')).toBeNull(); }); it('终态渲染可自解释的结论(已应用 / 已跳过 / 失败保留兜底名)', () => { const { unmount } = render( , ); const appliedRow = document.querySelector( '[data-task-source="project-naming"]', ) as HTMLElement | null; expect(within(appliedRow!).getByText('已完成')).not.toBeNull(); expect( within(appliedRow!).getByText('已应用 AI 名称「星河远征」。'), ).not.toBeNull(); unmount(); render( , ); const skippedRow = document.querySelector( '[data-task-source="project-naming"]', ) as HTMLElement | null; expect( within(skippedRow!).getByText( '当前名称已不是本次创建的兜底名(用户已手动改名),已跳过 AI 命名。', ), ).not.toBeNull(); }); it('失败行显示失败徽章与原因,并计入在途计数', () => { const { unmount } = render( , ); const row = document.querySelector( '[data-task-source="project-naming"]', ) as HTMLElement | null; expect(within(row!).getByText('失败')).not.toBeNull(); expect( within(row!).getByText( 'AI 命名请求失败或没有返回可用名称,已保留兜底名。', ), ).not.toBeNull(); expect(within(row!).getByRole('alert').textContent).toContain( '自动项目命名超时', ); unmount(); // 收起态:在途的命名任务要出现在右上角开关的计数里(与素材任务同一个计数)。 render( , ); expect(screen.getByLabelText('生成任务').textContent).toContain( '生成任务 · 1', ); }); });