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'
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
2026-07-22 23:13:55 +08:00
if ( path === '/api/v1/status' ) return route . fulfill ( { json : { status : 'unavailable' } } )
2026-07-22 18:34:20 +08:00
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' } )
2026-07-22 23:13:55 +08:00
await page . waitForSelector ( '.mini-connection.offline' )
const offlineConnection = await page . locator ( '.mini-connection' ) . evaluate ( ( element ) => ( {
text : element . textContent ,
borderColor : getComputedStyle ( element ) . borderColor ,
} ) )
2026-07-22 18:34:20 +08:00
await page . screenshot ( { path : 'test-results/mini-login.png' , fullPage : true } )
2026-07-22 19:03:24 +08:00
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' )
2026-07-22 18:34:20 +08:00
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 = [ ]
2026-07-22 23:13:55 +08:00
if ( ! offlineConnection . text ? . includes ( '连接异常' ) || offlineConnection . borderColor !== 'rgb(245, 63, 63)' ) failures . push ( ` offline connection must be red: ${ JSON . stringify ( offlineConnection ) } ` )
2026-07-22 19:03:24 +08:00
if ( loginDefaults [ 1 ] !== 'demo@senlin.ai' || loginDefaults [ 2 ] !== 'password123' ) failures . push ( ` login defaults failed: ${ JSON . stringify ( loginDefaults ) } ` )
2026-07-22 18:34:20 +08:00
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' ) )