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 expert = { id: '019b0000-0000-7000-8000-000000000102', slug: 'product-manager', category: 'product', categoryName: '产品', name: '产品经理', description: '负责需求分析、路线规划和产品交付。', emoji: '🧭', color: '#165DFF', } const workspace = { project: { id: projectId, name: '森林项目', identifier: 'forest', icon: 'folder', background: '#165DFF', description: '让项目计划、资料和 AI 协作保持在同一条线上。', initials: '森林', unreadCount: 0 }, channels: [], tags: [{ id: 'tag-product', name: '产品' }, { id: 'tag-design', name: '设计' }], recentSessions: [], inbox: [], tasks: [ { id: 'task-1', projectId, title: '整理产品路线', summary: '确认下一阶段交付范围。', completed: false, owner: '张明', due: null, createdAt: '今天 09:00', completedAt: null, tagId: 'tag-product', tag: '产品' }, { id: 'task-2', projectId, title: '检查迷你布局', summary: '验证窄屏下的导航和滚动。', completed: false, owner: '张明', due: null, createdAt: '今天 10:00', completedAt: null, tagId: 'tag-design', tag: '设计' }, ], aiSessions: [{ id: 'old-session', projectId, title: '梳理版本计划', summary: '项目会话', updatedAt: '今天', references: [] }], notesSources: [ { id: 'note-1', projectId, kind: 'note', title: '版本规划', updatedAt: '今天', tag: '产品', source: '项目笔记' }, { id: 'source-1', projectId, kind: 'file', title: '需求说明.pdf', updatedAt: '昨天', tag: '资料', source: '上传文件' }, ], cronPlans: [{ id: 'cron-1', projectId, title: '每周复盘', schedule: '0 17 * * 5', nextRun: null, enabled: true, lastResult: '', owner: '张明' }], } 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/status') return route.fulfill({ json: { status: 'unavailable' } }) if (path === '/api/v1/auth/login') return route.fulfill({ json: { token: 'mini-token' } }) if (path === '/api/v1/projects') { if (request.method() === 'POST') return route.fulfill({ status: 201, json: workspace.project }) 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, { ...expert, id: 'expert-2', name: 'UI 设计师', category: 'design', categoryName: '设计', emoji: '🎨', color: '#722ED1' }] }) if (path === `/api/v1/projects/${projectId}/ai-sessions`) { if (request.method() === 'POST') { const input = request.postDataJSON() return route.fulfill({ status: 201, json: { id: 'session-new', projectId, title: input.title, context: input.context, status: 'ready', expert, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } }) } return route.fulfill({ json: [{ id: 'session-1', projectId, title: '梳理版本计划', context: '下一步怎么安排', status: 'ready', expert, createdAt: '2026-07-22T10:00:00Z', updatedAt: '2026-07-22T10:00:00Z' }] }) } if (path.includes('/tasks/') && request.method() === 'PATCH') return route.fulfill({ json: {} }) if (path.endsWith('/tasks') && request.method() === 'POST') return route.fulfill({ status: 201, json: {} }) if (path.endsWith('/cron-plans') && request.method() === 'POST') return route.fulfill({ status: 201, 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.waitForSelector('.mini-connection.offline') const offlineConnection = await page.locator('.mini-connection').evaluate((element) => ({ text: element.textContent, borderColor: getComputedStyle(element).borderColor, })) await page.screenshot({ path: 'test-results/mini-login.png', fullPage: true }) const loginDefaults = await page.locator('.login-note input').evaluateAll((inputs) => inputs.map((input) => input.value)) await page.locator('.login-note input').nth(2).press('Enter') await page.waitForSelector('.mini-shell') await page.waitForTimeout(400) await page.screenshot({ path: 'test-results/mini-home-right.png', fullPage: true }) const home = await page.evaluate(() => ({ width: document.querySelector('.mini-shell')?.getBoundingClientRect().width, navCount: document.querySelectorAll('.mini-nav button').length, dock: document.documentElement.dataset.dock, overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth, title: document.querySelector('.page-intro h4')?.textContent ?? '', })) await page.getByRole('button', { name: '停靠左侧' }).click() const leftDock = await page.evaluate(() => document.documentElement.dataset.dock) await page.getByRole('button', { name: '计划', exact: true }).click() await page.getByRole('button', { name: '设计', exact: true }).click() const tasks = await page.evaluate(() => ({ count: document.querySelectorAll('.task-note').length, title: document.querySelector('.task-note strong')?.textContent ?? '', })) await page.screenshot({ path: 'test-results/mini-tasks.png', fullPage: true }) await page.getByRole('button', { name: '资料', exact: true }).click() const notes = await page.locator('.source-note').count() await page.getByRole('button', { name: 'AI', exact: true }).click() await page.getByRole('button', { name: /产品经理/ }).click() await page.locator('.mini-composer textarea').fill('请给出下一步计划') const sendEnabled = await page.getByRole('button', { name: '开始会话' }).isEnabled() await page.screenshot({ path: 'test-results/mini-ai.png', fullPage: true }) await browser.close() await server.close() const failures = [] if (!offlineConnection.text?.includes('连接异常') || offlineConnection.borderColor !== 'rgb(245, 63, 63)') failures.push(`offline connection must be red: ${JSON.stringify(offlineConnection)}`) if (loginDefaults[1] !== 'demo@senlin.ai' || loginDefaults[2] !== 'password123') failures.push(`login defaults failed: ${JSON.stringify(loginDefaults)}`) if (errors.length) failures.push(`console errors: ${errors.join('; ')}`) if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`) if (home.navCount !== 5) failures.push(`expected five top navigation items, got ${home.navCount}`) if (home.dock !== 'right' || leftDock !== 'left') failures.push(`dock state failed: right=${home.dock}, left=${leftDock}`) if (home.overflowX) failures.push('mini client has horizontal overflow') if (home.title !== '森林项目') failures.push(`project overview missing, got ${home.title}`) if (tasks.count !== 1 || tasks.title !== '检查迷你布局') failures.push(`task tag filtering failed: ${JSON.stringify(tasks)}`) if (notes !== 2) failures.push(`expected two note cards, got ${notes}`) if (!sendEnabled) failures.push('AI composer must enable after selecting an expert and entering a prompt') console.log(JSON.stringify({ home, leftDock, tasks, notes, sendEnabled, errors }, null, 2)) if (failures.length) throw new Error(failures.join('\n'))