2026-07-22 18:34:20 +08:00
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'
2026-07-25 21:52:19 +08:00
const user = { email : 'demo@senlin.ai' , displayName : '演示用户' }
2026-07-22 18:34:20 +08:00
const workspace = {
2026-07-25 21:52:19 +08:00
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' } ] ,
2026-07-22 18:34:20 +08:00
tasks : [
2026-07-25 21:52:19 +08:00
{ 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 : '设计' } ,
2026-07-22 18:34:20 +08:00
] ,
2026-07-25 21:52:19 +08:00
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' } ] ,
2026-07-22 18:34:20 +08:00
cronPlans : [ { id : 'cron-1' , projectId , title : '每周复盘' , schedule : '0 17 * * 5' , nextRun : null , enabled : true , lastResult : '' , owner : '张明' } ] ,
}
2026-07-25 21:52:19 +08:00
const expert = { id : 'expert-1' , slug : 'product-manager' , category : 'product' , categoryName : '产品' , name : '产品经理' , description : '负责需求分析' , emoji : '📘' , color : '#165DFF' }
2026-07-25 22:59:35 +08:00
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' }
2026-07-22 18:34:20 +08:00
await page . route ( 'http://localhost:9150/api/v1/**' , async ( route ) => {
2026-07-25 21:52:19 +08:00
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 ] } )
2026-07-22 18:34:20 +08:00
if ( path === ` /api/v1/projects/ ${ projectId } /workspace ` ) return route . fulfill ( { json : workspace } )
2026-07-25 21:52:19 +08:00
if ( path === '/api/v1/ai-experts' ) return route . fulfill ( { json : [ expert ] } )
2026-07-25 22:59:35 +08:00
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 } } )
2026-07-22 18:34:20 +08:00
if ( path . includes ( '/tasks/' ) && request . method ( ) === 'PATCH' ) return route . fulfill ( { json : { } } )
2026-07-25 21:52:19 +08:00
return route . fulfill ( { status : 404 , json : { error : { code : 'not_found' , message : '未配置的检查接口' } } } )
2026-07-22 18:34:20 +08:00
} )
await page . goto ( ` http://127.0.0.1: ${ port } / ` , { waitUntil : 'networkidle' } )
2026-07-25 21:52:19 +08:00
await page . locator ( '.login-card input' ) . nth ( 2 ) . press ( 'Enter' )
await page . waitForSelector ( '.app-shell' )
await page . waitForSelector ( '.task-row' )
2026-07-25 20:14:58 +08:00
await page . screenshot ( { path : 'test-results/app-home.png' , fullPage : true } )
2026-07-22 18:34:20 +08:00
const home = await page . evaluate ( ( ) => ( {
2026-07-25 21:52:19 +08:00
width : document . querySelector ( '.app-shell' ) ? . getBoundingClientRect ( ) . width ,
2026-07-25 22:23:34 +08:00
selectedAll : document . querySelector ( '.project-current span' ) ? . textContent ,
2026-07-25 21:52:19 +08:00
navCount : document . querySelectorAll ( '.channel-rail button' ) . length ,
2026-07-25 22:23:34 +08:00
statCount : document . querySelectorAll ( '.stat-card' ) . length ,
2026-07-25 21:52:19 +08:00
pending : document . querySelectorAll ( '.task-row:not(.done)' ) . length ,
completed : document . querySelectorAll ( '.task-row.done' ) . length ,
2026-07-22 18:34:20 +08:00
} ) )
2026-07-25 21:52:19 +08:00
await page . locator ( '.task-row' ) . first ( ) . click ( )
const taskModal = await page . getByText ( '编辑任务' ) . count ( )
await page . getByRole ( 'button' , { name : '取消' } ) . click ( )
2026-07-25 22:23:34 +08:00
await page . getByRole ( 'button' , { name : '探索' } ) . click ( )
2026-07-25 22:59:35 +08:00
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 } )
2026-07-25 22:23:34 +08:00
await page . locator ( '.project-current' ) . click ( )
await page . getByRole ( 'option' , { name : '森林项目' } ) . click ( )
await page . waitForSelector ( '.channel-rail' )
2026-07-25 21:52:19 +08:00
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 ( )
2026-07-25 20:14:58 +08:00
await page . screenshot ( { path : 'test-results/app-ai.png' , fullPage : true } )
2026-07-25 21:52:19 +08:00
await browser . close ( ) ; await server . close ( )
2026-07-22 18:34:20 +08:00
const failures = [ ]
if ( home . width !== 420 ) failures . push ( ` expected 420px shell, got ${ home . width } ` )
2026-07-25 22:23:34 +08:00
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 } ` )
2026-07-25 22:40:29 +08:00
if ( home . pending !== 1 || home . completed !== 0 ) failures . push ( ` all-project task sections incorrect: ${ JSON . stringify ( home ) } ` )
2026-07-25 21:52:19 +08:00
if ( taskModal !== 1 ) failures . push ( 'task editor did not open' )
2026-07-25 22:59:35 +08:00
if ( exploreSources !== 2 || exploreDetail !== 1 ) failures . push ( ` explore reader did not render: ${ JSON . stringify ( { exploreSources , exploreDetail } )} ` )
2026-07-25 21:52:19 +08:00
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 ( '; ' ) } ` )
2026-07-25 22:59:35 +08:00
console . log ( JSON . stringify ( { home , documents , experts , taskModal , exploreSources , exploreDetail , profileMenu , errors } , null , 2 ) )
2026-07-22 18:34:20 +08:00
if ( failures . length ) throw new Error ( failures . join ( '\n' ) )