import { createServer } from 'vite' import { chromium } from 'playwright' const port = Number(process.env.MINI_VISUAL_PORT ?? 4180) const server = await createServer({ root: process.cwd(), server: { host: '127.0.0.1', port, strictPort: true } }) await server.listen() const browser = await chromium.launch({ headless: true }) const page = await browser.newPage({ viewport: { width: 420, height: 820 }, deviceScaleFactor: 1 }) const errors = [] page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()) }) const projectId = '019b0000-0000-7000-8000-000000000101' const user = { email: 'demo@senlin.ai', displayName: '演示用户' } const workspace = { project: { id: projectId, name: '森林项目', identifier: 'forest', icon: 'folder', background: '#165DFF', description: '项目协作空间', initials: '森林', unreadCount: 0 }, channels: [], tags: [{ id: 'tag-product', name: '产品' }, { id: 'tag-design', name: '设计' }], recentSessions: [], inbox: [{ id: 'inbox-1', projectId, source: 'manual', title: '客户反馈', summary: '整理下周的反馈内容', status: 'open', tag: '产品', time: '2026-07-22T10:00:00Z' }], tasks: [ { id: 'task-1', projectId, title: '整理产品路线', summary: '确认下一阶段交付范围。', completed: false, owner: '张明', due: null, createdAt: '2026-07-22T09:00:00Z', completedAt: null, tagId: 'tag-product', tag: '产品' }, { id: 'task-2', projectId, title: '完成迷你布局', summary: '验证窄屏下的导航和滚动。', completed: true, owner: '张明', due: null, createdAt: '2026-07-21T10:00:00Z', completedAt: '2026-07-22T10:00:00Z', tagId: 'tag-design', tag: '设计' }, ], aiSessions: [{ id: 'session-1', projectId, title: '梳理版本计划', summary: '下一步怎么安排', updatedAt: '2026-07-22T10:00:00Z', references: [] }], documents: [{ id: 'doc-1', projectId, kind: 'markdown', name: '版本规划', extension: '.md', mimeType: 'text/markdown', updatedAt: '2026-07-22T10:00:00Z' }], cronPlans: [{ id: 'cron-1', projectId, title: '每周复盘', schedule: '0 17 * * 5', nextRun: null, enabled: true, lastResult: '', owner: '张明' }], } const expert = { id: 'expert-1', slug: 'product-manager', category: 'product', categoryName: '产品', name: '产品经理', description: '负责需求分析', emoji: '📘', color: '#165DFF' } await page.route('http://localhost:9150/api/v1/**', async (route) => { const request = route.request(); const path = new URL(request.url()).pathname if (path === '/api/v1/auth/login') return route.fulfill({ json: { token: 'app-token', user } }) if (path === '/api/v1/auth/me') return route.fulfill({ json: user }) if (path === '/api/v1/projects') return route.fulfill({ json: [workspace.project] }) if (path === `/api/v1/projects/${projectId}/workspace`) return route.fulfill({ json: workspace }) if (path === '/api/v1/ai-experts') return route.fulfill({ json: [expert] }) if (path.includes('/tasks/') && request.method() === 'PATCH') return route.fulfill({ json: {} }) return route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '未配置的检查接口' } } }) }) await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' }) await page.locator('.login-card input').nth(2).press('Enter') await page.waitForSelector('.app-shell') await page.waitForSelector('.task-row') await page.screenshot({ path: 'test-results/app-home.png', fullPage: true }) const home = await page.evaluate(() => ({ width: document.querySelector('.app-shell')?.getBoundingClientRect().width, selectedAll: document.querySelector('.project-current span')?.textContent, navCount: document.querySelectorAll('.channel-rail button').length, statCount: document.querySelectorAll('.stat-card').length, pending: document.querySelectorAll('.task-row:not(.done)').length, completed: document.querySelectorAll('.task-row.done').length, })) await page.locator('.task-row').first().click() const taskModal = await page.getByText('编辑任务').count() await page.getByRole('button', { name: '取消' }).click() await page.getByRole('button', { name: '探索' }).click() const exploreProjects = await page.locator('.project-explore-card').count() await page.locator('.project-current').click() await page.getByRole('option', { name: '森林项目' }).click() await page.waitForSelector('.channel-rail') await page.locator('.channel-rail button[title="资料"]').click() const documents = await page.locator('.detail-row').count() await page.locator('.channel-rail button[title="AI"]').click() await page.waitForSelector('.expert-chips') const experts = await page.locator('.expert-chips span').count() await page.locator('.profile-trigger').click() await page.getByText('修改资料').waitFor() const profileMenu = await page.getByText('修改资料').count() await page.screenshot({ path: 'test-results/app-ai.png', fullPage: true }) await browser.close(); await server.close() const failures = [] if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`) if (home.selectedAll !== '全部项目') failures.push(`default project scope must be 全部项目, got ${home.selectedAll}`) if (home.navCount !== 0) failures.push(`all-projects view must hide the right rail, got ${home.navCount}`) if (home.statCount !== 4) failures.push(`expected four summary cards, got ${home.statCount}`) if (home.pending !== 1 || home.completed !== 0) failures.push(`all-project task sections incorrect: ${JSON.stringify(home)}`) if (taskModal !== 1) failures.push('task editor did not open') if (exploreProjects !== 1) failures.push(`expected one project in explore, got ${exploreProjects}`) if (documents !== 1) failures.push(`expected one document, got ${documents}`) if (experts !== 1) failures.push(`expected one AI expert, got ${experts}`) if (profileMenu !== 1) failures.push('profile menu did not open') if (errors.length) failures.push(`console errors: ${errors.join('; ')}`) console.log(JSON.stringify({ home, documents, experts, taskModal, exploreProjects, profileMenu, errors }, null, 2)) if (failures.length) throw new Error(failures.join('\n'))