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' } const datasetSource = { id: 'source-1', name: '行业资讯', kind: 'rss', url: 'https://example.com/feed', iconUrl: '', description: '行业动态', enabled: true, builtIn: false, lastSyncedAt: null, itemCount: 1, createdAt: '2026-07-22T10:00:00Z', updatedAt: '2026-07-22T10:00:00Z' } const datasetItem = { id: 'item-1', sourceId: 'source-1', title: 'AI 工作台趋势', summary: '新的协作方式正在改变项目管理。', content: '这是来自数据源的探索内容,可在 Web 工作台继续管理数据源和同步任务。', url: '', imageUrl: '', status: 'unread', starred: false, publishedAt: '2026-07-22T10:00:00Z', createdAt: '2026-07-22T10:00:00Z', updatedAt: '2026-07-22T10:00:00Z' } 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 === '/api/v1/dataset-sources') return route.fulfill({ json: [datasetSource] }) if (path === '/api/v1/dataset-items') return route.fulfill({ json: { items: [datasetItem], total: 1, limit: 50, offset: 0 } }) 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() await page.waitForSelector('.explore-detail') const exploreSources = await page.locator('.explore-source-strip button').count() const exploreDetail = await page.locator('.explore-detail').count() await page.screenshot({ path: 'test-results/app-explore.png', fullPage: true }) 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() const documentActions = await page.locator('.document-actions .arco-btn').count() await page.locator('.document-actions .arco-btn').nth(1).click() await page.waitForSelector('.markdown-editor-page') await page.screenshot({ path: 'test-results/app-markdown-new.png', fullPage: true }) const markdownEditor = await page.locator('.markdown-editor-page').count() const markdownLayout = await page.evaluate(() => { const rect = (selector) => { const node = document.querySelector(selector); const value = node?.getBoundingClientRect(); return value ? { left: value.left, width: value.width } : null } return { scrollX: window.scrollX, scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, shell: rect('.app-shell'), main: rect('.app-main'), rail: rect('.channel-rail'), editor: rect('.markdown-editor-page') } }) await page.locator('.markdown-editor-page .content-heading .arco-btn').click() 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 (exploreSources !== 2 || exploreDetail !== 1) failures.push(`explore reader did not render: ${JSON.stringify({ exploreSources, exploreDetail })}`) if (documents !== 1) failures.push(`expected one document, got ${documents}`) if (documentActions !== 2 || markdownEditor !== 1) failures.push(`document actions did not render: ${JSON.stringify({ documentActions, markdownEditor })}`) 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, documentActions, markdownEditor, markdownLayout, experts, taskModal, exploreSources, exploreDetail, profileMenu, errors }, null, 2)) if (failures.length) throw new Error(failures.join('\n'))