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 = [ ]
page . on ( 'console' , ( message ) => {
if ( message . type ( ) === 'error' ) errors . push ( message . text ( ) )
} )
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-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' )
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' )
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 ,
}
: 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-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-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-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 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' } )
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 ( )
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 ( )
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 11:49:01 +08:00
console . log ( JSON . stringify ( { workspaceMetrics , projectMetrics , 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-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-20 11:43:02 +08:00
if ( ! metrics . statusOnlineDot ) failures . push ( 'expected online status to render as an icon dot' )
if ( metrics . statusName && metrics . statusOnlineDot && Math . abs ( metrics . statusName . top - metrics . statusOnlineDot . top ) > 6 ) {
failures . push ( ` expected online dot to sit beside username, got name= ${ JSON . stringify ( metrics . statusName ) } , dot= ${ JSON . stringify ( metrics . statusOnlineDot ) } ` )
}
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' )
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
}
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 ) } ` )
}
if ( ! projectMetrics . hasProjectOverview ) failures . push ( 'expected first project click to show project overview page' )
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' )
if ( ! projectMetrics . projectRail || ! projectMetrics . channelSidebar || ! projectMetrics . stage || ! projectMetrics . inspector ) {
2026-07-20 10:00:47 +08:00
failures . push (
2026-07-20 11:49:01 +08:00
` missing project layout regions: rail= ${ JSON . stringify ( projectMetrics . projectRail ) } , sidebar= ${ JSON . stringify ( projectMetrics . channelSidebar ) } , stage= ${ JSON . stringify ( projectMetrics . stage ) } , inspector= ${ JSON . stringify ( projectMetrics . inspector ) } ` ,
2026-07-20 10:00:47 +08:00
)
2026-07-20 11:49:01 +08:00
} else {
const sameTop = [ projectMetrics . projectRail , projectMetrics . channelSidebar , projectMetrics . stage , projectMetrics . inspector ]
. every ( ( region ) => Math . abs ( region . top - projectMetrics . projectRail . top ) <= 1 )
if ( ! sameTop ) {
failures . push (
` expected project rail, channel sidebar, stage, and inspector to share the same top edge, got rail= ${ projectMetrics . projectRail . top } , sidebar= ${ projectMetrics . channelSidebar . top } , stage= ${ projectMetrics . stage . top } , inspector= ${ projectMetrics . inspector . top } ` ,
)
}
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 } ` )
}
if ( Math . abs ( projectMetrics . inspector . left - projectMetrics . stage . right ) > 1 ) {
failures . push ( ` expected inspector to sit after stage, got inspector.left= ${ projectMetrics . inspector . left } , stage.right= ${ projectMetrics . stage . right } ` )
}
if ( Math . abs ( projectMetrics . inspector . right - projectMetrics . viewportWidth ) > 1 ) {
failures . push ( ` expected inspector to end at viewport right edge, got inspector.right= ${ projectMetrics . inspector . right } , viewport= ${ projectMetrics . viewportWidth } ` )
}
2026-07-20 10:00:47 +08:00
}
if ( failures . length ) {
throw new Error ( failures . join ( '\n' ) )
}