119 lines
7.7 KiB
JavaScript
119 lines
7.7 KiB
JavaScript
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'))
|