2026-07-20 09:50:55 +08:00
import { createServer } from 'vite'
import { chromium } from 'playwright'
const port = Number ( process . env . VISUAL _CHECK _PORT ? ? 4175 )
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 : 1440 , height : 1024 } , deviceScaleFactor : 1 } )
const errors = [ ]
2026-07-22 15:25:22 +08:00
const projectId = '019b0000-0000-7000-8000-000000000001'
2026-07-22 17:09:54 +08:00
const visualExperts = [
{
id : '019b0000-0000-7000-8000-000000000020' , slug : 'product-manager' , category : 'product' , categoryName : '产品' ,
name : '产品经理' , description : '负责需求分析、路线规划和产品交付。' , emoji : '🧭' , color : '#165DFF' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000021' , slug : 'ui-designer' , category : 'design' , categoryName : '设计' ,
name : 'UI 设计师' , description : '负责界面设计、组件规范和视觉一致性。' , emoji : '🎨' , color : '#722ED1' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000022' , slug : 'frontend-developer' , category : 'engineering' , categoryName : '工程' ,
name : '前端开发者' , description : '负责现代 Web 应用实现与性能优化。' , emoji : '💻' , color : '#0FC6C2' ,
} ,
]
2026-07-23 15:07:35 +08:00
let visualDatasetSources = [
{
2026-07-23 15:14:31 +08:00
id : '019b0000-0000-7000-8000-000000000030' , name : '手动收集' , kind : 'manual' , url : '' , iconUrl : '' ,
2026-07-23 22:25:12 +08:00
description : '手动收集的文章与线索' , enabled : true , builtIn : false , lastSyncedAt : null , itemCount : 1 ,
2026-07-23 15:07:35 +08:00
createdAt : '2026-07-22T06:00:00Z' , updatedAt : '2026-07-22T06:00:00Z' ,
} ,
{
2026-07-23 15:14:31 +08:00
id : '019b0000-0000-7000-8000-000000000031' , name : '需求文档' , kind : 'link' , url : 'https://example.com/requirements' , iconUrl : '' ,
2026-07-23 22:25:12 +08:00
description : '产品需求与业务文档' , enabled : true , builtIn : false , lastSyncedAt : null , itemCount : 1 ,
2026-07-23 15:07:35 +08:00
createdAt : '2026-07-22T06:00:00Z' , updatedAt : '2026-07-22T06:00:00Z' ,
} ,
{
2026-07-23 15:14:31 +08:00
id : '019b0000-0000-7000-8000-000000000032' , name : '架构讨论' , kind : 'link' , url : 'https://example.com/architecture' , iconUrl : '' ,
2026-07-23 22:25:12 +08:00
description : '架构方案与系统设计讨论' , enabled : true , builtIn : false , lastSyncedAt : null , itemCount : 0 ,
2026-07-23 15:07:35 +08:00
createdAt : '2026-07-22T06:00:00Z' , updatedAt : '2026-07-22T06:00:00Z' ,
} ,
]
let visualDatasetItems = [
{
id : '019b0000-0000-7000-8000-000000000040' , sourceId : visualDatasetSources [ 0 ] . id ,
title : '森林项目体验优化需求' , summary : '整理工作台信息层级,并沉淀为可跟进计划。' , content : '探索数据条目的正文内容。' ,
url : 'https://example.com/article-1' , status : 'unread' , starred : false , publishedAt : '2026-07-22T08:00:00Z' ,
createdAt : '2026-07-22T08:00:00Z' , updatedAt : '2026-07-22T08:00:00Z' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000041' , sourceId : visualDatasetSources [ 1 ] . id ,
title : '项目资料组织方案' , summary : '围绕项目上下文统一组织任务、笔记、资料和 AI 会话。' , content : '资料组织方案正文。' ,
url : 'https://example.com/article-2' , status : 'read' , starred : true , publishedAt : '2026-07-22T07:00:00Z' ,
createdAt : '2026-07-22T07:00:00Z' , updatedAt : '2026-07-22T07:00:00Z' ,
} ,
]
2026-07-20 09:50:55 +08:00
page . on ( 'console' , ( message ) => {
if ( message . type ( ) === 'error' ) errors . push ( message . text ( ) )
} )
2026-07-22 15:25:22 +08:00
const visualCheckWorkspace = {
project : {
id : projectId ,
name : '森林项目' ,
identifier : 'forest' ,
icon : '森' ,
background : '#165DFF' ,
description : '前后端 API 对齐视觉检查项目' ,
initials : '森林' ,
unreadCount : 2 ,
} ,
channels : [
{ id : 'overview' , projectId , type : 'overview' , title : '概况' , icon : 'home' , count : 0 , url : '' , sortOrder : 0 } ,
2026-07-22 15:44:41 +08:00
{ id : 'inbox' , projectId , type : 'inbox' , title : 'Inbox 消息流' , icon : 'email' , count : 2 , url : '' , sortOrder : 1 } ,
2026-07-22 15:25:22 +08:00
{ id : 'tasks' , projectId , type : 'tasks' , title : '工作计划' , icon : 'list' , count : 0 , url : '' , sortOrder : 1 } ,
2026-07-22 16:26:05 +08:00
{ id : 'ai' , projectId , type : 'ai_sessions' , title : 'AI 会话' , icon : 'robot' , count : 0 , url : '' , sortOrder : 2 } ,
2026-07-22 15:25:22 +08:00
{ id : 'notes' , projectId , type : 'notes_sources' , title : '笔记资料' , icon : 'file' , count : 0 , url : '' , sortOrder : 3 } ,
{ id : 'cron' , projectId , type : 'cron' , title : '计划任务' , icon : 'clock' , count : 0 , url : '' , sortOrder : 4 } ,
] ,
2026-07-22 16:26:05 +08:00
tags : [
{ id : '019b0000-0000-7000-8000-000000000002' , name : '产品' } ,
{ id : '019b0000-0000-7000-8000-000000000005' , name : '设计' } ,
] ,
2026-07-22 15:25:22 +08:00
recentSessions : [ ] ,
2026-07-22 15:39:57 +08:00
inbox : [
{
id : '019b0000-0000-7000-8000-000000000003' ,
projectId ,
source : '需求文档' ,
title : '森林项目体验优化需求' ,
summary : '整理工作台信息层级,并把关键需求沉淀为可跟进的项目计划。' ,
status : 'open' ,
tag : '需求' ,
time : '2026-07-22T08:00:00Z' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000004' ,
projectId ,
source : '架构讨论' ,
title : '项目资料组织方案' ,
summary : '围绕项目上下文统一组织任务、笔记、资料和 AI 会话。' ,
status : 'open' ,
tag : '架构' ,
time : '2026-07-22T07:00:00Z' ,
} ,
] ,
2026-07-22 16:26:05 +08:00
tasks : [
{
id : '019b0000-0000-7000-8000-000000000006' ,
projectId ,
title : '梳理产品需求' ,
summary : '确认首版工作台的功能边界。' ,
completed : false ,
owner : '森林AI' ,
createdAt : '2026-07-22' ,
completedAt : '' ,
duration : '' ,
progress : '40%' ,
tagId : '019b0000-0000-7000-8000-000000000002' ,
tag : '产品' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000007' ,
projectId ,
title : '优化会话界面' ,
summary : '统一 AI 会话的阅读与输入体验。' ,
completed : false ,
owner : '森林AI' ,
createdAt : '2026-07-22' ,
completedAt : '' ,
duration : '' ,
progress : '60%' ,
tagId : '019b0000-0000-7000-8000-000000000005' ,
tag : '设计' ,
} ,
{
id : '019b0000-0000-7000-8000-000000000008' ,
projectId ,
title : '确认项目定位' ,
summary : '完成项目功能与亮点说明。' ,
completed : true ,
owner : '森林AI' ,
createdAt : '2026-07-21' ,
completedAt : '2026-07-22' ,
duration : '1 天' ,
progress : '100%' ,
tagId : '019b0000-0000-7000-8000-000000000002' ,
tag : '产品' ,
} ,
] ,
2026-07-22 15:25:22 +08:00
aiSessions : [ ] ,
notesSources : [ ] ,
cronPlans : [ ] ,
}
await page . route ( 'http://localhost:9150/api/v1/**' , async ( route ) => {
const url = new URL ( route . request ( ) . url ( ) )
if ( url . pathname === '/api/v1/status' ) {
await route . fulfill ( { json : { timestamp : new Date ( ) . toISOString ( ) } } )
return
}
if ( url . pathname === '/api/v1/auth/login' ) {
await route . fulfill ( { json : { token : 'visual-check-token' } } )
return
}
if ( url . pathname === '/api/v1/projects' ) {
await route . fulfill ( { json : [ visualCheckWorkspace . project ] } )
return
}
if ( url . pathname === ` /api/v1/projects/ ${ projectId } /workspace ` ) {
await route . fulfill ( { json : visualCheckWorkspace } )
return
}
if ( url . pathname === ` /api/v1/projects/ ${ projectId } /ai-sessions ` ) {
2026-07-22 16:26:05 +08:00
if ( route . request ( ) . method ( ) === 'POST' ) {
const input = route . request ( ) . postDataJSON ( )
2026-07-22 17:09:54 +08:00
const expert = visualExperts . find ( ( item ) => item . id === input . expertId )
2026-07-22 16:26:05 +08:00
await route . fulfill ( {
json : {
id : '019b0000-0000-7000-8000-000000000010' ,
projectId ,
title : input . title ,
context : input . context ,
status : 'ready' ,
2026-07-22 17:09:54 +08:00
expert ,
2026-07-22 16:26:05 +08:00
createdAt : '2026-07-22T09:00:00Z' ,
updatedAt : '2026-07-22T09:00:00Z' ,
} ,
} )
return
}
await route . fulfill ( {
json : [ {
id : '019b0000-0000-7000-8000-000000000009' ,
projectId ,
title : '报价分析' ,
context : '分析当前方案的报价结构。' ,
status : 'ready' ,
2026-07-22 17:09:54 +08:00
expert : visualExperts [ 1 ] ,
2026-07-22 16:26:05 +08:00
createdAt : '2026-07-22T08:30:00Z' ,
updatedAt : '2026-07-22T08:30:00Z' ,
} ] ,
} )
2026-07-22 15:25:22 +08:00
return
}
2026-07-22 17:09:54 +08:00
if ( url . pathname === '/api/v1/ai-experts' ) {
await route . fulfill ( { json : visualExperts } )
return
}
2026-07-23 15:07:35 +08:00
if ( url . pathname === '/api/v1/dataset-sources' && route . request ( ) . method ( ) === 'GET' ) {
await route . fulfill ( { json : visualDatasetSources } )
return
}
if ( url . pathname === '/api/v1/dataset-sources' && route . request ( ) . method ( ) === 'POST' ) {
const input = route . request ( ) . postDataJSON ( )
const created = {
id : ` 019b0000-0000-7000-8000- ${ String ( 50 + visualDatasetSources . length ) . padStart ( 12 , '0' ) } ` ,
... input ,
2026-07-23 22:25:12 +08:00
builtIn : false ,
2026-07-23 15:07:35 +08:00
lastSyncedAt : null ,
itemCount : 0 ,
createdAt : new Date ( ) . toISOString ( ) ,
updatedAt : new Date ( ) . toISOString ( ) ,
}
visualDatasetSources = [ ... visualDatasetSources , created ]
await route . fulfill ( { status : 201 , json : created } )
return
}
if ( url . pathname . startsWith ( '/api/v1/dataset-sources/' ) && route . request ( ) . method ( ) === 'PATCH' ) {
const id = url . pathname . split ( '/' ) . at ( - 1 )
const input = route . request ( ) . postDataJSON ( )
const current = visualDatasetSources . find ( ( source ) => source . id === id )
const updated = { ... current , ... input , updatedAt : new Date ( ) . toISOString ( ) }
visualDatasetSources = visualDatasetSources . map ( ( source ) => source . id === id ? updated : source )
await route . fulfill ( { json : updated } )
return
}
if ( url . pathname . startsWith ( '/api/v1/dataset-sources/' ) && route . request ( ) . method ( ) === 'DELETE' ) {
const id = url . pathname . split ( '/' ) . at ( - 1 )
visualDatasetSources = visualDatasetSources . filter ( ( source ) => source . id !== id )
visualDatasetItems = visualDatasetItems . filter ( ( item ) => item . sourceId !== id )
await route . fulfill ( { status : 204 , body : '' } )
return
}
if ( url . pathname === '/api/v1/dataset-items' && route . request ( ) . method ( ) === 'GET' ) {
2026-07-23 22:25:12 +08:00
const sourceId = url . searchParams . get ( 'sourceId' )
const offset = Number ( url . searchParams . get ( 'offset' ) ? ? 0 )
const limit = Number ( url . searchParams . get ( 'limit' ) ? ? 50 )
const matchingItems = sourceId
? visualDatasetItems . filter ( ( item ) => item . sourceId === sourceId )
: visualDatasetItems
await route . fulfill ( {
json : {
items : matchingItems . slice ( offset , offset + limit ) ,
total : matchingItems . length ,
offset ,
limit ,
} ,
} )
return
}
if ( url . pathname . endsWith ( '/deposit' ) && route . request ( ) . method ( ) === 'POST' ) {
await route . fulfill ( { status : 201 , json : { noteId : '019b0000-0000-7000-8000-000000000099' , projectId } } )
2026-07-23 15:07:35 +08:00
return
}
if ( url . pathname . startsWith ( '/api/v1/dataset-items/' ) && route . request ( ) . method ( ) === 'PATCH' ) {
const id = url . pathname . split ( '/' ) . at ( - 1 )
const input = route . request ( ) . postDataJSON ( )
const current = visualDatasetItems . find ( ( item ) => item . id === id )
const updated = { ... current , ... input , updatedAt : new Date ( ) . toISOString ( ) }
visualDatasetItems = visualDatasetItems . map ( ( item ) => item . id === id ? updated : item )
await route . fulfill ( { json : updated } )
return
}
2026-07-23 23:08:25 +08:00
if ( url . pathname === '/api/v1/dataset-sources/sync' ) {
2026-07-23 15:07:35 +08:00
await route . fulfill ( {
2026-07-23 20:47:54 +08:00
status : 200 ,
2026-07-23 23:08:25 +08:00
json : visualDatasetSources . map ( ( source ) => ( {
2026-07-23 15:07:35 +08:00
sourceId : source . id ,
2026-07-23 20:47:54 +08:00
status : 'completed' ,
2026-07-23 23:08:25 +08:00
result : 'format=rss fetched=2 inserted=0' ,
2026-07-23 15:07:35 +08:00
} ) ) ,
} )
return
}
2026-07-22 15:25:22 +08:00
await route . fulfill ( { status : 404 , json : { error : { code : 'not_found' , message : '视觉检查未配置该接口' } } } )
} )
2026-07-20 11:49:01 +08:00
const collectMetrics = async ( ) => page . evaluate ( ( ) => {
2026-07-20 09:50:55 +08:00
const statusbar = document . querySelector ( '.statusbar' )
2026-07-20 11:43:02 +08:00
const statusName = document . querySelector ( '.status-name' )
const statusOnlineDot = document . querySelector ( '.status-online-dot' )
2026-07-22 15:42:58 +08:00
const statusUpgrade = document . querySelector ( '.status-upgrade' )
const statusSystem = document . querySelector ( '.status-system' )
2026-07-20 09:50:55 +08:00
const search = document . querySelector ( '.global-search' )
2026-07-20 11:03:27 +08:00
const main = document . querySelector ( '.workbench-main' )
2026-07-20 11:40:45 +08:00
const workspacePage = document . querySelector ( '.workspace-page' )
2026-07-20 11:49:01 +08:00
const projectOverview = document . querySelector ( '.overview-page:not(.workspace-page)' )
2026-07-20 10:00:47 +08:00
const rail = document . querySelector ( '.project-rail' )
2026-07-20 10:13:05 +08:00
const railChildren = rail ? . querySelector ( '.arco-layout-sider-children' )
2026-07-20 11:03:27 +08:00
const sidebar = document . querySelector ( '.channel-sidebar' )
2026-07-20 12:13:57 +08:00
const channelList = document . querySelector ( '.channel-list' )
2026-07-20 11:03:27 +08:00
const stage = document . querySelector ( '.stage' )
const inspector = document . querySelector ( '.inspector' )
2026-07-20 11:57:32 +08:00
const workspaceActions = [ ... document . querySelectorAll ( '.workspace-page .overview-head .arco-btn' ) ]
. map ( ( button ) => button . textContent ? . trim ( ) )
2026-07-20 10:00:47 +08:00
const dashboard = document . querySelector ( '.dashboard-button' )
2026-07-21 00:46:09 +08:00
const workspaceExplore = document . querySelector ( '.dashboard-button[title="探索"]' )
2026-07-20 10:00:47 +08:00
const project = document . querySelector ( '.project-button' )
const create = document . querySelector ( '.create-project' )
2026-07-20 10:09:09 +08:00
const firstBadge = document . querySelector ( '.project-badge .arco-badge-number' )
const railItems = [ ... document . querySelectorAll ( '.dashboard-button, .project-button, .create-project' ) ]
2026-07-20 10:00:47 +08:00
const rect = ( node ) => {
const box = node ? . getBoundingClientRect ( )
2026-07-20 10:09:09 +08:00
return box ? { left : box . left , right : box . right , top : box . top , width : box . width , height : box . height } : null
2026-07-20 10:00:47 +08:00
}
2026-07-20 10:09:09 +08:00
const railBox = rail ? . getBoundingClientRect ( )
const itemRects = railItems . map ( rect ) . filter ( Boolean )
const horizontalInsets = railBox && project
? {
left : project . getBoundingClientRect ( ) . left - railBox . left ,
right : railBox . right - project . getBoundingClientRect ( ) . right ,
}
: null
const verticalGaps = itemRects . slice ( 1 ) . map ( ( item , index ) => item . top - ( itemRects [ index ] . top + itemRects [ index ] . height ) )
2026-07-20 10:13:05 +08:00
const overflowState = ( node ) => node
? {
clientWidth : node . clientWidth ,
scrollWidth : node . scrollWidth ,
clientHeight : node . clientHeight ,
scrollHeight : node . scrollHeight ,
overflowX : getComputedStyle ( node ) . overflowX ,
overflowY : getComputedStyle ( node ) . overflowY ,
2026-07-20 12:08:52 +08:00
scrollbarWidth : getComputedStyle ( node ) . scrollbarWidth ,
2026-07-20 10:13:05 +08:00
}
: null
2026-07-20 10:00:47 +08:00
2026-07-20 09:50:55 +08:00
return {
statusbarHeight : statusbar ? . getBoundingClientRect ( ) . height ,
2026-07-20 11:43:02 +08:00
statusName : rect ( statusName ) ,
statusOnlineDot : rect ( statusOnlineDot ) ,
2026-07-22 15:42:58 +08:00
statusUpgrade : rect ( statusUpgrade ) ,
statusSystemText : statusSystem ? . textContent ? . replace ( /\s+/g , ' ' ) . trim ( ) ? ? '' ,
2026-07-20 09:50:55 +08:00
searchHeight : search ? . getBoundingClientRect ( ) . height ,
2026-07-20 11:03:27 +08:00
workbenchMain : rect ( main ) ,
2026-07-20 11:40:45 +08:00
hasWorkspacePage : Boolean ( workspacePage ) ,
2026-07-20 11:49:01 +08:00
hasProjectOverview : Boolean ( projectOverview ) ,
2026-07-22 15:39:57 +08:00
exploreSourceCardCount : document . querySelectorAll ( '.explore-source-card' ) . length ,
hasExploreReader : Boolean ( document . querySelector ( '.explore-reader-layout' ) ) ,
2026-07-22 15:44:41 +08:00
hasInboxChannel : [ ... document . querySelectorAll ( '.channel-button' ) ] . some ( ( button ) => button . textContent ? . includes ( 'Inbox' ) ) ,
2026-07-20 10:00:47 +08:00
projectRailWidth : rail ? . getBoundingClientRect ( ) . width ,
2026-07-20 11:03:27 +08:00
projectRail : rect ( rail ) ,
channelSidebar : rect ( sidebar ) ,
stage : rect ( stage ) ,
inspector : rect ( inspector ) ,
2026-07-20 11:57:32 +08:00
workspaceActions ,
2026-07-20 10:00:47 +08:00
dashboardButton : rect ( dashboard ) ,
2026-07-20 11:40:45 +08:00
dashboardButtonActive : dashboard ? . classList . contains ( 'active' ) ? ? false ,
2026-07-22 15:50:16 +08:00
dashboardUsesPanelIcon : Boolean ( dashboard ? . querySelector ( '.arco-icon-dashboard' ) ) ,
dashboardUsesBrandImage : Boolean ( dashboard ? . querySelector ( 'img' ) ) ,
2026-07-21 00:46:09 +08:00
workspaceExploreButton : rect ( workspaceExplore ) ,
workspaceExploreButtonActive : workspaceExplore ? . classList . contains ( 'active' ) ? ? false ,
2026-07-20 10:00:47 +08:00
projectButton : rect ( project ) ,
2026-07-20 11:40:45 +08:00
projectButtonActive : project ? . classList . contains ( 'active' ) ? ? false ,
2026-07-20 10:00:47 +08:00
createProjectButton : rect ( create ) ,
2026-07-20 10:09:09 +08:00
firstProjectBadge : rect ( firstBadge ) ,
horizontalInsets ,
verticalGaps ,
2026-07-20 10:13:05 +08:00
projectRailOverflow : overflowState ( rail ) ,
projectRailChildrenOverflow : overflowState ( railChildren ) ,
2026-07-20 12:04:36 +08:00
channelSidebarOverflow : overflowState ( sidebar ) ,
2026-07-20 12:13:57 +08:00
channelListOverflow : overflowState ( channelList ) ,
2026-07-20 12:04:36 +08:00
stageOverflow : overflowState ( stage ) ,
inspectorOverflow : overflowState ( inspector ) ,
2026-07-20 09:50:55 +08:00
overflowX : document . documentElement . scrollWidth > document . documentElement . clientWidth ,
2026-07-20 11:03:27 +08:00
viewportWidth : window . innerWidth ,
2026-07-20 09:50:55 +08:00
}
} )
2026-07-20 11:49:01 +08:00
await page . goto ( ` http://127.0.0.1: ${ port } / ` , { waitUntil : 'networkidle' } )
2026-07-22 23:25:49 +08:00
await page . locator ( '.login-form input' ) . first ( ) . fill ( 'localhost:9150' )
await page . waitForSelector ( '.connection-card.online' )
const loginConnectionStatus = await page . locator ( '.connection-card' ) . textContent ( )
2026-07-20 11:49:01 +08:00
await page . screenshot ( { path : 'test-results/login-react-acro.png' , fullPage : true } )
await page . locator ( '.login-form-panel .arco-btn-primary' ) . click ( )
await page . waitForSelector ( '.workbench-shell' )
await page . waitForTimeout ( 700 )
await page . screenshot ( { path : 'test-results/workbench-react-acro-light.png' , fullPage : true } )
const workspaceMetrics = await collectMetrics ( )
2026-07-21 00:46:09 +08:00
await page . locator ( '.dashboard-button[title="探索"]' ) . click ( )
2026-07-20 18:25:04 +08:00
await page . waitForTimeout ( 500 )
2026-07-21 00:46:09 +08:00
await page . screenshot ( { path : 'test-results/workspace-explore-react-acro-light.png' , fullPage : true } )
const workspaceExploreMetrics = await collectMetrics ( )
2026-07-20 18:25:04 +08:00
2026-07-22 15:56:04 +08:00
await page . getByRole ( 'button' , { name : '添加数据源' } ) . click ( )
await page . waitForSelector ( '.explore-source-modal' )
await page . waitForTimeout ( 250 )
await page . screenshot ( { path : 'test-results/explore-add-source-modal.png' , fullPage : true } )
const addSourceModalCheck = await page . evaluate ( ( ) => ( {
title : document . querySelector ( '.explore-source-modal .arco-modal-title' ) ? . textContent ? ? '' ,
inputCount : document . querySelectorAll ( '.explore-source-modal input, .explore-source-modal textarea' ) . length ,
} ) )
await page . locator ( '.explore-source-modal input' ) . first ( ) . fill ( '行业资讯' )
2026-07-23 15:07:35 +08:00
await page . locator ( '.explore-source-modal input[placeholder="https://example.com/feed"]' ) . fill ( 'https://example.com/feed' )
2026-07-22 15:56:04 +08:00
await page . locator ( '.explore-source-modal .arco-modal-footer .arco-btn-primary' ) . click ( )
const addedSourceVisible = await page . locator ( '.explore-source-card' , { hasText : '行业资讯' } ) . count ( ) === 1
await page . locator ( '.explore-source-card' ) . nth ( 1 ) . hover ( )
await page . getByRole ( 'button' , { name : '编辑手动收集' } ) . click ( )
await page . waitForSelector ( '.explore-source-modal' )
await page . waitForTimeout ( 250 )
await page . screenshot ( { path : 'test-results/explore-edit-source-modal.png' , fullPage : true } )
const editSourceModalCheck = await page . evaluate ( ( ) => ( {
title : document . querySelector ( '.explore-source-modal .arco-modal-title' ) ? . textContent ? ? '' ,
name : document . querySelector ( '.explore-source-modal input' ) ? . value ? ? '' ,
} ) )
await page . locator ( '.explore-source-modal input' ) . first ( ) . fill ( '手动整理' )
await page . locator ( '.explore-source-modal .arco-modal-footer .arco-btn-primary' ) . click ( )
2026-07-23 21:34:13 +08:00
const editedSourceCard = page . locator ( '.explore-source-card' , { hasText : '手动整理' } )
await editedSourceCard . waitFor ( { state : 'visible' } )
const editedSourceVisible = await editedSourceCard . count ( ) === 1
2026-07-22 15:56:04 +08:00
2026-07-20 11:49:01 +08:00
await page . locator ( '.project-button' ) . first ( ) . click ( )
await page . waitForTimeout ( 500 )
await page . screenshot ( { path : 'test-results/project-react-acro-light.png' , fullPage : true } )
const projectMetrics = await collectMetrics ( )
2026-07-20 12:08:52 +08:00
await page . locator ( '.channel-sidebar' ) . hover ( )
const channelSidebarHoverMetrics = await collectMetrics ( )
await page . locator ( '.stage' ) . hover ( )
const stageHoverMetrics = await collectMetrics ( )
2026-07-20 12:13:57 +08:00
await page . locator ( '.channel-list' ) . hover ( )
const channelListHoverMetrics = await collectMetrics ( )
2026-07-20 12:08:52 +08:00
2026-07-20 12:26:21 +08:00
const channelPageChecks = [ ]
2026-07-22 16:26:05 +08:00
let taskTagFilterCheck = null
let aiSessionCheck = null
2026-07-20 12:26:21 +08:00
for ( const channel of [
{ label : '工作计划' , pageClass : 'project-tasks-page' , expectedHeading : '工作计划' } ,
2026-07-22 16:26:05 +08:00
{ label : 'AI 会话' , pageClass : 'project-ai-page' , expectedHeading : '' } ,
2026-07-20 12:26:21 +08:00
{ label : '笔记资料' , pageClass : 'project-notes-page' , expectedHeading : '笔记资料' } ,
2026-07-21 00:46:09 +08:00
{ label : '计划任务' , pageClass : 'project-cron-page' , expectedHeading : '计划任务' } ,
{ label : '新建频道' , pageClass : 'project-new-channel-page' , expectedHeading : '新建频道' } ,
2026-07-20 12:26:21 +08:00
] ) {
await page . locator ( '.channel-button' , { hasText : channel . label } ) . click ( )
await page . waitForTimeout ( 250 )
2026-07-22 16:26:05 +08:00
const pageCheck = await page . evaluate ( ( expected ) => {
2026-07-20 12:26:21 +08:00
const pageNode = document . querySelector ( ` . ${ expected . pageClass } ` )
const heading = pageNode ? . querySelector ( 'h4, h5' ) ? . textContent ? ? ''
const activeChannel = document . querySelector ( '.channel-button.active' ) ? . textContent ? ? ''
return {
... expected ,
foundPage : Boolean ( pageNode ) ,
heading ,
activeChannel ,
2026-07-22 15:48:43 +08:00
inputCount : pageNode ? . querySelectorAll ( 'input, textarea' ) . length ? ? 0 ,
hasSaveChannel : [ ... ( pageNode ? . querySelectorAll ( 'button' ) ? ? [ ] ) ] . some ( ( button ) => button . textContent ? . includes ( '保存频道' ) ) ,
2026-07-22 16:26:05 +08:00
hasOverviewHead : Boolean ( pageNode ? . querySelector ( '.overview-head' ) ) ,
hasSessionList : Boolean ( pageNode ? . querySelector ( '.agent-session-list' ) ) ,
hasComposer : Boolean ( pageNode ? . querySelector ( '.agent-composer' ) ) ,
2026-07-22 17:09:54 +08:00
hasExpertPicker : Boolean ( pageNode ? . querySelector ( '.agent-expert-picker' ) ) ,
expertHeading : pageNode ? . querySelector ( '.agent-expert-heading' ) ? . textContent ? . replace ( /\s+/g , ' ' ) . trim ( ) ? ? '' ,
2026-07-20 12:26:21 +08:00
}
2026-07-22 16:26:05 +08:00
} , channel )
if ( channel . label === '工作计划' ) {
await page . getByRole ( 'button' , { name : '设计' , exact : true } ) . click ( )
await page . waitForTimeout ( 150 )
taskTagFilterCheck = await page . evaluate ( ( ) => ( {
activeTag : document . querySelector ( '.task-tag-filter.active' ) ? . textContent ? . trim ( ) ? ? '' ,
pendingTitles : [ ... document . querySelectorAll ( '.task-plan-title' ) ] . map ( ( node ) => node . textContent ? . trim ( ) ) ,
completedTitles : [ ... document . querySelectorAll ( '.completed-plan-title' ) ] . map ( ( node ) => node . textContent ? . trim ( ) ) ,
pressed : document . querySelector ( '.task-tag-filter[aria-pressed="true"]' ) ? . textContent ? . trim ( ) ? ? '' ,
} ) )
await page . screenshot ( { path : 'test-results/project-task-tag-filter-light.png' , fullPage : true } )
}
if ( channel . label === 'AI 会话' ) {
2026-07-22 17:09:54 +08:00
await page . screenshot ( { path : 'test-results/project-ai-expert-picker-light.png' , fullPage : true } )
await page . getByRole ( 'button' , { name : /产品经理/ } ) . click ( )
2026-07-22 16:26:05 +08:00
await page . locator ( '.agent-composer textarea' ) . fill ( '请分析这个项目的下一步计划' )
await page . getByRole ( 'button' , { name : '发送消息' } ) . click ( )
await page . waitForSelector ( '.agent-user-message' )
aiSessionCheck = await page . evaluate ( ( ) => ( {
userMessage : document . querySelector ( '.agent-user-message' ) ? . textContent ? . trim ( ) ? ? '' ,
activeSession : document . querySelector ( '.agent-session.active' ) ? . textContent ? . trim ( ) ? ? '' ,
composerValue : document . querySelector ( '.agent-composer textarea' ) ? . value ? ? '' ,
2026-07-22 17:09:54 +08:00
expertName : document . querySelector ( '.agent-assistant-name' ) ? . textContent ? . trim ( ) ? ? '' ,
2026-07-22 16:26:05 +08:00
} ) )
await page . screenshot ( { path : 'test-results/project-ai-session-light.png' , fullPage : true } )
}
channelPageChecks . push ( pageCheck )
2026-07-20 12:26:21 +08:00
}
2026-07-20 11:49:01 +08:00
await page . locator ( '.topbar-actions .arco-btn' ) . first ( ) . click ( )
await page . screenshot ( { path : 'test-results/project-react-acro-dark.png' , fullPage : true } )
2026-07-20 09:50:55 +08:00
await browser . close ( )
await server . close ( )
2026-07-20 12:13:57 +08:00
console . log ( JSON . stringify ( {
workspaceMetrics ,
2026-07-21 00:46:09 +08:00
workspaceExploreMetrics ,
2026-07-22 15:56:04 +08:00
addSourceModalCheck ,
addedSourceVisible ,
editSourceModalCheck ,
editedSourceVisible ,
2026-07-20 12:13:57 +08:00
projectMetrics ,
channelSidebarHoverMetrics ,
stageHoverMetrics ,
channelListHoverMetrics ,
2026-07-20 12:26:21 +08:00
channelPageChecks ,
2026-07-22 16:26:05 +08:00
taskTagFilterCheck ,
aiSessionCheck ,
2026-07-20 12:13:57 +08:00
errors ,
} , null , 2 ) )
2026-07-20 10:00:47 +08:00
const failures = [ ]
2026-07-20 11:49:01 +08:00
const metrics = workspaceMetrics
2026-07-22 23:25:49 +08:00
if ( ! loginConnectionStatus ? . includes ( '连接正常' ) ) failures . push ( ` scheme-less server address must connect, got ${ JSON . stringify ( loginConnectionStatus ) } ` )
2026-07-20 10:00:47 +08:00
if ( errors . length ) failures . push ( ` console errors: ${ errors . join ( '; ' ) } ` )
if ( metrics . statusbarHeight !== 32 ) failures . push ( ` expected statusbar height 32, got ${ metrics . statusbarHeight } ` )
2026-07-22 15:25:22 +08:00
if ( ! metrics . statusName ) failures . push ( 'expected authenticated status label' )
2026-07-22 15:42:58 +08:00
if ( ! metrics . statusOnlineDot ) failures . push ( 'expected legacy online status dot' )
if ( ! metrics . statusUpgrade ) failures . push ( 'expected legacy upgrade action' )
if ( ! metrics . statusSystemText . includes ( '3 个计划进行中' ) || ! metrics . statusSystemText . includes ( 'AI 空闲' ) || ! metrics . statusSystemText . includes ( '152GB 可用' ) ) {
failures . push ( ` expected legacy system status summary, got ${ JSON . stringify ( metrics . statusSystemText ) } ` )
}
2026-07-20 10:00:47 +08:00
if ( metrics . searchHeight !== 42 ) failures . push ( ` expected search height 42, got ${ metrics . searchHeight } ` )
if ( metrics . overflowX ) failures . push ( 'expected no horizontal overflow' )
2026-07-20 11:03:27 +08:00
if ( ! metrics . workbenchMain ) failures . push ( 'missing workbench main' )
2026-07-20 11:40:45 +08:00
if ( ! metrics . hasWorkspacePage ) failures . push ( 'expected login to land on workspace page' )
if ( ! metrics . dashboardButtonActive ) failures . push ( 'expected dashboard button to be active after login' )
2026-07-22 15:50:16 +08:00
if ( ! metrics . dashboardUsesPanelIcon || metrics . dashboardUsesBrandImage ) failures . push ( 'expected workspace entry to use the panel icon instead of the brand logo' )
2026-07-21 00:46:09 +08:00
if ( ! metrics . workspaceExploreButton ) failures . push ( 'expected fixed workspace explore button to render in project rail' )
if ( metrics . workspaceExploreButtonActive ) failures . push ( 'expected fixed workspace explore button not to be active after login' )
2026-07-20 11:40:45 +08:00
if ( metrics . projectButtonActive ) failures . push ( 'expected first project button not to be active on workspace landing page' )
2026-07-20 11:49:01 +08:00
if ( metrics . channelSidebar ) failures . push ( ` expected workspace to hide channel sidebar, got ${ JSON . stringify ( metrics . channelSidebar ) } ` )
if ( metrics . inspector ) failures . push ( ` expected workspace to hide inspector, got ${ JSON . stringify ( metrics . inspector ) } ` )
2026-07-20 11:57:32 +08:00
if ( metrics . workspaceActions . some ( ( text ) => text ? . includes ( '团队视图' ) || text ? . includes ( '进入重点项目' ) ) ) {
failures . push ( ` expected workspace header actions to remove team view and focused project buttons, got ${ JSON . stringify ( metrics . workspaceActions ) } ` )
}
2026-07-20 11:49:01 +08:00
if ( ! metrics . projectRail || ! metrics . stage ) {
failures . push ( ` missing workspace layout regions: rail= ${ JSON . stringify ( metrics . projectRail ) } , stage= ${ JSON . stringify ( metrics . stage ) } ` )
2026-07-20 11:03:27 +08:00
} else {
2026-07-20 11:49:01 +08:00
if ( Math . abs ( metrics . stage . left - metrics . projectRail . right ) > 1 ) {
failures . push ( ` expected workspace stage to start after project rail, got stage.left= ${ metrics . stage . left } , rail.right= ${ metrics . projectRail . right } ` )
2026-07-20 11:03:27 +08:00
}
2026-07-20 11:49:01 +08:00
if ( Math . abs ( metrics . stage . right - metrics . viewportWidth ) > 1 ) {
failures . push ( ` expected workspace stage to end at viewport right edge, got stage.right= ${ metrics . stage . right } , viewport= ${ metrics . viewportWidth } ` )
2026-07-20 11:03:27 +08:00
}
}
2026-07-20 10:00:47 +08:00
if ( ! metrics . projectRailWidth || metrics . projectRailWidth < 88 ) {
failures . push ( ` expected project rail at least 88px wide, got ${ metrics . projectRailWidth } ` )
}
if ( ! metrics . projectButton ) failures . push ( 'missing project button' )
2026-07-20 10:09:09 +08:00
if ( ! metrics . horizontalInsets || Math . abs ( metrics . horizontalInsets . left - metrics . horizontalInsets . right ) > 1 ) {
failures . push ( ` expected equal rail horizontal insets, got ${ JSON . stringify ( metrics . horizontalInsets ) } ` )
}
if ( metrics . verticalGaps . some ( ( gap ) => gap !== 12 ) ) {
failures . push ( ` expected all project rail vertical gaps to be 12px, got ${ JSON . stringify ( metrics . verticalGaps ) } ` )
}
2026-07-20 10:13:05 +08:00
for ( const [ name , overflow ] of [
[ 'project rail' , metrics . projectRailOverflow ] ,
[ 'project rail children' , metrics . projectRailChildrenOverflow ] ,
] ) {
2026-07-20 11:49:01 +08:00
if ( ! overflow || overflow . overflowX !== 'hidden' || overflow . overflowY !== 'hidden' ) {
2026-07-20 10:13:05 +08:00
failures . push ( ` expected ${ name } scrollbars to be hidden with no overflow, got ${ JSON . stringify ( overflow ) } ` )
}
}
2026-07-20 10:09:09 +08:00
if (
! metrics . firstProjectBadge ||
metrics . firstProjectBadge . right > metrics . projectRailWidth ||
metrics . firstProjectBadge . left < 0
) {
failures . push ( ` expected first project badge to stay inside the project rail, got ${ JSON . stringify ( metrics . firstProjectBadge ) } ` )
}
2026-07-20 10:00:47 +08:00
if (
! metrics . dashboardButton ||
metrics . dashboardButton . width !== metrics . projectButton ? . width ||
metrics . dashboardButton . height !== metrics . projectButton ? . height
) {
2026-07-20 11:49:01 +08:00
failures . push ( ` dashboard button size ${ JSON . stringify ( metrics . dashboardButton ) } does not match project button ${ JSON . stringify ( metrics . projectButton ) } ` )
2026-07-20 10:00:47 +08:00
}
2026-07-20 18:25:04 +08:00
if (
2026-07-21 00:46:09 +08:00
! metrics . workspaceExploreButton ||
metrics . workspaceExploreButton . width !== metrics . projectButton ? . width ||
metrics . workspaceExploreButton . height !== metrics . projectButton ? . height
2026-07-20 18:25:04 +08:00
) {
2026-07-21 00:46:09 +08:00
failures . push ( ` workspace explore button size ${ JSON . stringify ( metrics . workspaceExploreButton ) } does not match project button ${ JSON . stringify ( metrics . projectButton ) } ` )
2026-07-20 18:25:04 +08:00
}
2026-07-20 10:00:47 +08:00
if (
! metrics . createProjectButton ||
metrics . createProjectButton . width !== metrics . projectButton ? . width ||
metrics . createProjectButton . height !== metrics . projectButton ? . height
) {
2026-07-20 11:49:01 +08:00
failures . push ( ` create project button size ${ JSON . stringify ( metrics . createProjectButton ) } does not match project button ${ JSON . stringify ( metrics . projectButton ) } ` )
}
2026-07-21 00:46:09 +08:00
if ( ! workspaceExploreMetrics . workspaceExploreButtonActive ) failures . push ( 'expected fixed workspace explore button to become active when selected' )
if ( workspaceExploreMetrics . dashboardButtonActive ) failures . push ( 'expected dashboard button not to be active on workspace explore page' )
if ( workspaceExploreMetrics . channelSidebar ) failures . push ( ` expected workspace explore to hide channel sidebar, got ${ JSON . stringify ( workspaceExploreMetrics . channelSidebar ) } ` )
if ( workspaceExploreMetrics . inspector ) failures . push ( ` expected workspace explore to hide inspector, got ${ JSON . stringify ( workspaceExploreMetrics . inspector ) } ` )
2026-07-22 15:39:57 +08:00
if ( workspaceExploreMetrics . exploreSourceCardCount !== 4 ) failures . push ( ` expected four legacy explore source cards, got ${ workspaceExploreMetrics . exploreSourceCardCount } ` )
if ( ! workspaceExploreMetrics . hasExploreReader ) failures . push ( 'expected legacy explore article reader to render' )
2026-07-22 15:56:04 +08:00
if ( ! addSourceModalCheck . title . includes ( '添加数据源' ) || addSourceModalCheck . inputCount < 3 ) {
failures . push ( ` expected add data source modal with fields, got ${ JSON . stringify ( addSourceModalCheck ) } ` )
}
if ( ! addedSourceVisible ) failures . push ( 'expected saved data source to appear in the source cards' )
if ( ! editSourceModalCheck . title . includes ( '编辑数据源' ) || editSourceModalCheck . name !== '手动收集' ) {
failures . push ( ` expected prefilled edit data source modal, got ${ JSON . stringify ( editSourceModalCheck ) } ` )
}
if ( ! editedSourceVisible ) failures . push ( 'expected edited data source name to update its card' )
2026-07-21 00:46:09 +08:00
if ( ! workspaceExploreMetrics . stage || ! workspaceExploreMetrics . projectRail ) {
failures . push ( ` missing workspace explore layout regions: rail= ${ JSON . stringify ( workspaceExploreMetrics . projectRail ) } , stage= ${ JSON . stringify ( workspaceExploreMetrics . stage ) } ` )
} else if ( Math . abs ( workspaceExploreMetrics . stage . left - workspaceExploreMetrics . projectRail . right ) > 1 ) {
failures . push ( ` expected workspace explore stage to start after project rail, got stage.left= ${ workspaceExploreMetrics . stage . left } , rail.right= ${ workspaceExploreMetrics . projectRail . right } ` )
2026-07-20 18:25:04 +08:00
}
2026-07-20 11:49:01 +08:00
if ( ! projectMetrics . hasProjectOverview ) failures . push ( 'expected first project click to show project overview page' )
2026-07-22 15:44:41 +08:00
if ( projectMetrics . hasInboxChannel ) failures . push ( 'expected removed Inbox message stream to stay hidden from project channels' )
2026-07-20 11:49:01 +08:00
if ( projectMetrics . hasWorkspacePage ) failures . push ( 'expected project overview mode to leave workspace page' )
if ( ! projectMetrics . projectButtonActive ) failures . push ( 'expected first project button to be active in project mode' )
if ( projectMetrics . dashboardButtonActive ) failures . push ( 'expected dashboard button not to be active in project mode' )
2026-07-20 12:45:13 +08:00
if ( projectMetrics . inspector ) {
failures . push ( ` expected project page to remove right inspector until it becomes an on-demand panel, got ${ JSON . stringify ( projectMetrics . inspector ) } ` )
}
if ( ! projectMetrics . projectRail || ! projectMetrics . channelSidebar || ! projectMetrics . stage ) {
2026-07-20 10:00:47 +08:00
failures . push (
2026-07-20 12:45:13 +08:00
` missing project layout regions: rail= ${ JSON . stringify ( projectMetrics . projectRail ) } , sidebar= ${ JSON . stringify ( projectMetrics . channelSidebar ) } , stage= ${ JSON . stringify ( projectMetrics . stage ) } ` ,
2026-07-20 10:00:47 +08:00
)
2026-07-20 11:49:01 +08:00
} else {
2026-07-20 12:45:13 +08:00
const sameTop = [ projectMetrics . projectRail , projectMetrics . channelSidebar , projectMetrics . stage ]
2026-07-20 11:49:01 +08:00
. every ( ( region ) => Math . abs ( region . top - projectMetrics . projectRail . top ) <= 1 )
if ( ! sameTop ) {
failures . push (
2026-07-20 12:45:13 +08:00
` expected project rail, channel sidebar, and stage to share the same top edge, got rail= ${ projectMetrics . projectRail . top } , sidebar= ${ projectMetrics . channelSidebar . top } , stage= ${ projectMetrics . stage . top } ` ,
2026-07-20 11:49:01 +08:00
)
}
if ( Math . abs ( projectMetrics . channelSidebar . left - projectMetrics . projectRail . right ) > 1 ) {
failures . push ( ` expected channel sidebar to sit after project rail, got sidebar.left= ${ projectMetrics . channelSidebar . left } , rail.right= ${ projectMetrics . projectRail . right } ` )
}
if ( Math . abs ( projectMetrics . stage . left - projectMetrics . channelSidebar . right ) > 1 ) {
failures . push ( ` expected stage to sit after channel sidebar, got stage.left= ${ projectMetrics . stage . left } , sidebar.right= ${ projectMetrics . channelSidebar . right } ` )
}
2026-07-20 12:45:13 +08:00
if ( Math . abs ( projectMetrics . stage . right - projectMetrics . viewportWidth ) > 1 ) {
failures . push ( ` expected project stage to end at viewport right edge, got stage.right= ${ projectMetrics . stage . right } , viewport= ${ projectMetrics . viewportWidth } ` )
2026-07-20 11:49:01 +08:00
}
2026-07-20 10:00:47 +08:00
}
2026-07-20 12:04:36 +08:00
for ( const [ name , overflow ] of [
[ 'channel sidebar' , projectMetrics . channelSidebarOverflow ] ,
2026-07-20 12:13:57 +08:00
[ 'channel list' , projectMetrics . channelListOverflow ] ,
2026-07-20 12:04:36 +08:00
[ 'stage' , projectMetrics . stageOverflow ] ,
] ) {
if ( ! overflow || overflow . overflowX !== 'hidden' || overflow . overflowY !== 'auto' ) {
failures . push ( ` expected ${ name } to hide horizontal scrollbars and show vertical scrollbars only when needed, got ${ JSON . stringify ( overflow ) } ` )
}
2026-07-20 12:08:52 +08:00
if ( overflow ? . scrollbarWidth !== 'none' ) {
failures . push ( ` expected ${ name } scrollbar to be hidden until hover, got ${ JSON . stringify ( overflow ) } ` )
}
}
for ( const [ name , overflow ] of [
[ 'channel sidebar hover' , channelSidebarHoverMetrics . channelSidebarOverflow ] ,
2026-07-20 12:13:57 +08:00
[ 'channel list hover' , channelListHoverMetrics . channelListOverflow ] ,
2026-07-20 12:08:52 +08:00
[ 'stage hover' , stageHoverMetrics . stageOverflow ] ,
] ) {
if ( overflow ? . scrollbarWidth !== 'thin' ) {
failures . push ( ` expected ${ name } scrollbar to appear on hover, got ${ JSON . stringify ( overflow ) } ` )
}
2026-07-20 12:04:36 +08:00
}
2026-07-20 12:26:21 +08:00
for ( const check of channelPageChecks ) {
if ( ! check . foundPage ) {
failures . push ( ` expected ${ check . label } to render . ${ check . pageClass } ` )
}
2026-07-22 16:26:05 +08:00
if ( check . expectedHeading && ! check . heading . includes ( check . expectedHeading ) ) {
2026-07-20 12:26:21 +08:00
failures . push ( ` expected ${ check . label } heading to include ${ check . expectedHeading } , got ${ JSON . stringify ( check . heading ) } ` )
}
if ( ! check . activeChannel . includes ( check . label ) ) {
failures . push ( ` expected ${ check . label } sidebar button to stay active, got ${ JSON . stringify ( check . activeChannel ) } ` )
}
2026-07-22 15:48:43 +08:00
if ( check . label === '新建频道' && ( check . inputCount < 4 || ! check . hasSaveChannel ) ) {
failures . push ( ` expected legacy new channel editor, got inputs= ${ check . inputCount } , save= ${ check . hasSaveChannel } ` )
}
2026-07-22 17:09:54 +08:00
if ( check . label === 'AI 会话' && (
check . hasOverviewHead || ! check . hasSessionList || ! check . hasComposer || ! check . hasExpertPicker ||
! check . expertHeading . includes ( '选择专家进行会话' )
) ) {
2026-07-22 16:26:05 +08:00
failures . push ( ` expected headerless AI session chat layout, got ${ JSON . stringify ( check ) } ` )
}
}
if (
! taskTagFilterCheck ||
taskTagFilterCheck . activeTag !== '设计' ||
taskTagFilterCheck . pressed !== '设计' ||
taskTagFilterCheck . pendingTitles . join ( '|' ) !== '优化会话界面' ||
taskTagFilterCheck . completedTitles . length !== 0
) {
failures . push ( ` expected clickable task tags to filter pending and completed plans, got ${ JSON . stringify ( taskTagFilterCheck ) } ` )
}
if (
! aiSessionCheck ||
aiSessionCheck . userMessage !== '请分析这个项目的下一步计划' ||
! aiSessionCheck . activeSession . includes ( '请分析这个项目的下一步计划' ) ||
2026-07-22 17:09:54 +08:00
aiSessionCheck . composerValue !== '' ||
aiSessionCheck . expertName !== '产品经理'
2026-07-22 16:26:05 +08:00
) {
failures . push ( ` expected AI composer to create and select a session, got ${ JSON . stringify ( aiSessionCheck ) } ` )
2026-07-20 12:26:21 +08:00
}
2026-07-20 10:00:47 +08:00
if ( failures . length ) {
throw new Error ( failures . join ( '\n' ) )
}