import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; test.describe('AI Design workspace', () => { test('keeps Prompt Museum cards usable when relative media fails', async ({ launchElectronApp }) => { test.setTimeout(90_000); const app = await launchElectronApp({ imageWorkspaceMode: 'local', skipSetup: true, }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-painting').click(); const imageSidebar = page.getByTestId('sidebar-image-workspace'); await expect(imageSidebar.getByTestId('sidebar-open-image-prompt-museum')).toBeVisible(); await app.evaluate(({ ipcMain }) => { const image = { url: '/api/image-prompt-museum/e2e-entry/media/thumbnail', width: 1200, height: 900, alt: 'E2E museum thumbnail', }; const card = { id: 'e2e-entry', slug: 'e2e-entry', title: '相对媒体 E2E', summary: '验证图片失败不会阻断卡片。', thumbnail: image, categories: [], model: { id: 'e2e-model', name: 'E2E Model' }, language: 'zh-CN', attribution: { author: { name: 'E2E 作者' }, source: { name: 'E2E 来源' }, license: { name: '测试许可', attributionText: 'E2E attribution' }, }, publishedAt: '2026-08-18T00:00:00.000Z', updatedAt: '2026-08-18T00:00:00.000Z', }; const respond = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json }, }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => { const path = request.path ?? ''; if (path === '/api/works/image-prompt-museum') { return respond({ success: true, data: { items: [card], facets: { useCases: [], styles: [], subjects: [] }, nextCursor: null, total: 1, }, }); } if (path === '/api/works/image-prompt-museum/e2e-entry') { return respond({ success: true, data: { ...card, prompt: 'Create a resilient image card.', variables: [], images: [image], requiresReferenceImages: false, }, }); } if (path === '/api/works/image-prompt-museum/e2e-entry/media/thumbnail') { return respond({ mimeType: 'text/html', dataBase64: 'PGgxPnVuc2FmZTwvaDE+' }); } return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } }; }); }); await imageSidebar.getByTestId('sidebar-open-image-prompt-museum').click(); const cardButton = page.getByRole('button', { name: '查看 相对媒体 E2E' }); await expect(cardButton).toBeVisible(); await expect(page.getByLabel('E2E museum thumbnail加载失败')).toBeVisible(); await expect(cardButton).toBeEnabled(); await cardButton.click(); await expect(page.getByRole('button', { name: '关闭详情' })).toBeVisible(); } finally { await closeElectronApp(app); } }); test('keeps the conversation and generation task panes full height', async ({ launchElectronApp }) => { test.setTimeout(150_000); const app = await launchElectronApp({ imageWorkspaceMode: 'local', skipSetup: true, }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-painting').click(); const imageSidebar = page.getByTestId('sidebar-image-workspace'); await expect(imageSidebar.getByTestId('sidebar-open-image-prompt-museum')).toBeVisible(); await expect(imageSidebar.getByTestId('sidebar-create-image-project')).toBeVisible(); await page.getByTestId('sidebar-create-image-project').click(); await page.getByLabel('项目名称').fill('多会话验收项目'); await page.getByRole('button', { name: '创建项目' }).click(); const projectCard = page.locator('[data-testid^="sidebar-image-project-"]').first(); await expect(projectCard).not.toContainText('1 个会话'); await expect(projectCard).not.toContainText('设计会话'); await expect(page.getByRole('button', { name: '收起 多会话验收项目 的会话' })).toBeVisible(); await expect(page.getByRole('button', { name: '重命名 多会话验收项目' })).toBeVisible(); await expect(page.locator('[data-testid^="sidebar-image-conversations-"]').first()) .not.toHaveClass(/border-t/); await expect(page.getByTestId('image-workspace-conversation')).toBeVisible(); await expect(page.getByTestId('image-workspace-conversation-fade')).toBeVisible(); await expect(page.getByTestId('design-task-list')).toBeVisible(); await expect(page.getByTestId('design-task-fade')).toBeVisible(); await expect(page.getByTestId('image-canvas-conversation-select')).toHaveCount(0); const emptyState = page.getByTestId('image-workspace-empty-state'); await expect(emptyState).toContainText('今天一起描绘什么?'); await expect(emptyState.getByRole('img', { name: 'Makelore' })).toHaveCount(1); const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByRole('button', { name: '上传参考图' }).click(); await fileChooserPromise; await expect(page.getByRole('dialog', { name: '选择视频首帧' })).toHaveCount(0); const layoutBounds = await page.evaluate(() => { const readBounds = (testId: string) => { const element = document.querySelector(`[data-testid="${testId}"]`); if (!element) return null; const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return { top: rect.top, bottom: rect.bottom, borderLeftWidth: style.borderLeftWidth, overflowY: style.overflowY, }; }; return { shell: readBounds('main-layout'), canvas: readBounds('image-canvas-page'), conversation: readBounds('image-workspace-conversation-pane'), tasks: readBounds('design-task-list'), taskScroll: readBounds('design-task-scroll'), }; }); expect(layoutBounds.shell).not.toBeNull(); expect(layoutBounds.canvas).not.toBeNull(); expect(layoutBounds.conversation).not.toBeNull(); expect(layoutBounds.tasks).not.toBeNull(); expect(layoutBounds.taskScroll).not.toBeNull(); expect(Math.abs(layoutBounds.canvas!.top - layoutBounds.shell!.top)).toBeLessThanOrEqual(1); expect(Math.abs(layoutBounds.canvas!.bottom - layoutBounds.shell!.bottom)).toBeLessThanOrEqual(1); expect(Math.abs(layoutBounds.conversation!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1); expect(Math.abs(layoutBounds.conversation!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1); expect(Math.abs(layoutBounds.tasks!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1); expect(Math.abs(layoutBounds.tasks!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1); expect(layoutBounds.tasks!.borderLeftWidth).toBe('1px'); expect(layoutBounds.taskScroll!.overflowY).toBe('auto'); const firstPrompt = '为项目设计一张蓝色星空海报'; await page.getByLabel('设计需求').fill(firstPrompt); await page.getByLabel('设计需求').press('Enter'); await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt); await expect(page.getByText('设计 Agent', { exact: true })).toHaveCount(0); await expect(page.getByText('你', { exact: true })).toHaveCount(0); await expect(page.getByTestId('image-workspace-message').first()).toHaveClass(/w-full/); const firstQuote = page.locator('[data-testid^="design-quote-"]').first(); await expect(firstQuote.getByRole('button', { name: '继续调整' })).toBeVisible(); await expect(firstQuote.getByTestId('design-quote-model')).toHaveCount(0); await expect(firstQuote).not.toContainText('提示词模式'); await page.getByRole('button', { name: '确认并开始生成' }).click(); await expect(page.getByRole('button', { name: '确认生成' })).toHaveCount(0); await expect(page.getByRole('button', { name: '继续调整' })).toHaveCount(0); const task = page .locator('[data-testid^="design-task-local-task-"]') .filter({ hasText: firstPrompt }) .first(); await expect(task).toContainText(firstPrompt); const firstConversation = page.locator('[data-testid^="sidebar-image-conversation-"]').first(); await expect(firstConversation).toContainText('图片任务已创建,可在右侧任务列表查看进度。'); await expect(firstConversation).not.toContainText('新会话'); const firstConversationTestId = await page .locator('[data-testid^="sidebar-image-conversation-"]') .first() .getAttribute('data-testid'); expect(firstConversationTestId).toBeTruthy(); await projectCard.hover(); await page.getByRole('button', { name: '新建设计会话' }).click(); await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(2); const secondPrompt = '这一条会话只讨论红色角色动态视频'; await page.getByLabel('设计需求').fill(secondPrompt); await page.getByRole('button', { name: '发送给设计 Agent' }).click(); await expect(page.getByTestId('image-workspace-conversation')).toContainText(secondPrompt); await expect(page.getByTestId('design-quote-final-prompt')).toHaveValue(secondPrompt); await page.getByTestId('design-quote-final-prompt').fill('用户编辑后的动态视频完整提示词'); await expect(page.getByTestId('design-quote-model')).toHaveCount(0); await page.getByTestId('design-quote-resolution').selectOption('720P'); await page.getByTestId('design-quote-aspect-ratio').selectOption('9:16'); await page.getByTestId('design-quote-duration').selectOption('4'); await expect(page.getByRole('button', { name: '重新报价中' })).toBeDisabled(); await expect(page.getByRole('button', { name: '确认并开始生成' })).toBeEnabled({ timeout: 10_000 }); await page.getByRole('button', { name: '确认并开始生成' }).click(); await expect(page.getByRole('button', { name: '确认并开始生成' })).toHaveCount(0); await page.getByTestId(firstConversationTestId!).click(); await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt); await expect(page.getByTestId('image-workspace-conversation')).not.toContainText(secondPrompt); // The local adapter deterministically advances queued -> running -> succeeded as the // task fallback reconciles. The delete confirmation also explains what happens to // queued and running tasks on the server. await task.getByRole('button', { name: '查看生图任务详情' }).click(); const taskDetails = page.getByRole('dialog', { name: '生成内容详情' }); await expect(taskDetails).toContainText('已完成', { timeout: 40_000 }); await taskDetails.getByRole('button', { name: '关闭生成内容详情' }).click(); await projectCard.hover(); await page.getByRole('button', { name: '删除 多会话验收项目' }).click(); const deleteDialog = page.getByRole('dialog', { name: '删除项目' }); await expect(deleteDialog).toContainText('未提交的任务会被取消并释放预留积分'); await expect(deleteDialog).toContainText('已提交或运行中的任务会继续后台结算'); await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled(); await expect(projectCard).toBeVisible(); await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收'); await expect(deleteDialog.getByText('项目名称必须完全匹配。')).toBeVisible(); await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled(); await expect(projectCard).toBeVisible(); await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收项目'); await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeEnabled(); await deleteDialog.getByTestId('confirm-delete-image-project').click(); await expect(deleteDialog).toHaveCount(0); await expect(page.locator('[data-testid^="sidebar-image-project-"]')).toHaveCount(0); await expect(imageSidebar).toContainText('还没有设计项目'); await expect(page.getByRole('heading', { name: '创建第一个设计项目' })).toBeVisible(); await imageSidebar.getByTestId('sidebar-open-image-prompt-museum').click(); await expect(page.getByTestId('image-prompt-museum-page')).toBeVisible(); await expect(page.getByRole('heading', { name: '获取灵感' })).toBeVisible(); await expect(page.getByText('提示词博物馆', { exact: true })).toHaveCount(0); await expect(page.getByTestId('titlebar-page-title')).toHaveText('获取灵感'); await expect(page.getByTestId('titlebar-logo')).toHaveCount(1); await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/sticky/); await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/top-0/); } finally { await closeElectronApp(app); } }); });