Files
agent/apps/senlinai-acro-react/scripts/visual-check.mjs

205 lines
9.6 KiB
JavaScript

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())
})
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 })
await page.locator('.topbar-actions .arco-btn').first().click()
await page.screenshot({ path: 'test-results/workbench-react-acro-dark.png', fullPage: true })
const metrics = await page.evaluate(() => {
const statusbar = document.querySelector('.statusbar')
const statusName = document.querySelector('.status-name')
const statusOnlineDot = document.querySelector('.status-online-dot')
const search = document.querySelector('.global-search')
const main = document.querySelector('.workbench-main')
const workspacePage = document.querySelector('.workspace-page')
const rail = document.querySelector('.project-rail')
const railChildren = rail?.querySelector('.arco-layout-sider-children')
const sidebar = document.querySelector('.channel-sidebar')
const sidebarTitle = document.querySelector('.project-title h5')
const stage = document.querySelector('.stage')
const inspectorTitle = document.querySelector('.inspector-title h5')
const inspector = document.querySelector('.inspector')
const dashboard = document.querySelector('.dashboard-button')
const project = document.querySelector('.project-button')
const create = document.querySelector('.create-project')
const firstBadge = document.querySelector('.project-badge .arco-badge-number')
const railItems = [...document.querySelectorAll('.dashboard-button, .project-button, .create-project')]
const rect = (node) => {
const box = node?.getBoundingClientRect()
return box ? { left: box.left, right: box.right, top: box.top, width: box.width, height: box.height } : null
}
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))
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
return {
statusbarHeight: statusbar?.getBoundingClientRect().height,
statusName: rect(statusName),
statusOnlineDot: rect(statusOnlineDot),
searchHeight: search?.getBoundingClientRect().height,
workbenchMain: rect(main),
hasWorkspacePage: Boolean(workspacePage),
projectRailWidth: rail?.getBoundingClientRect().width,
projectRail: rect(rail),
channelSidebar: rect(sidebar),
sidebarTitleText: sidebarTitle?.textContent ?? '',
stage: rect(stage),
inspector: rect(inspector),
inspectorTitleText: inspectorTitle?.textContent ?? '',
dashboardButton: rect(dashboard),
dashboardButtonActive: dashboard?.classList.contains('active') ?? false,
projectButton: rect(project),
projectButtonActive: project?.classList.contains('active') ?? false,
createProjectButton: rect(create),
firstProjectBadge: rect(firstBadge),
horizontalInsets,
verticalGaps,
projectRailOverflow: overflowState(rail),
projectRailChildrenOverflow: overflowState(railChildren),
overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
viewportWidth: window.innerWidth,
}
})
await browser.close()
await server.close()
console.log(JSON.stringify({ metrics, errors }, null, 2))
const failures = []
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
if (metrics.statusbarHeight !== 32) failures.push(`expected statusbar height 32, got ${metrics.statusbarHeight}`)
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)}`)
}
if (metrics.searchHeight !== 42) failures.push(`expected search height 42, got ${metrics.searchHeight}`)
if (metrics.overflowX) failures.push('expected no horizontal overflow')
if (!metrics.workbenchMain) failures.push('missing workbench main')
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')
if (!metrics.sidebarTitleText.includes('工作台')) {
failures.push(`expected sidebar title to describe workspace landing page, got ${JSON.stringify(metrics.sidebarTitleText)}`)
}
if (!metrics.inspectorTitleText.includes('工作台')) {
failures.push(`expected inspector title to describe workspace landing page, got ${JSON.stringify(metrics.inspectorTitleText)}`)
}
if (!metrics.projectRail || !metrics.channelSidebar || !metrics.stage || !metrics.inspector) {
failures.push(
`missing main layout regions: rail=${JSON.stringify(metrics.projectRail)}, sidebar=${JSON.stringify(metrics.channelSidebar)}, stage=${JSON.stringify(metrics.stage)}, inspector=${JSON.stringify(metrics.inspector)}`,
)
} else {
const sameTop = [metrics.projectRail, metrics.channelSidebar, metrics.stage, metrics.inspector]
.every((region) => Math.abs(region.top - metrics.projectRail.top) <= 1)
if (!sameTop) {
failures.push(
`expected project rail, channel sidebar, stage, and inspector to share the same top edge, got rail=${metrics.projectRail.top}, sidebar=${metrics.channelSidebar.top}, stage=${metrics.stage.top}, inspector=${metrics.inspector.top}`,
)
}
if (Math.abs(metrics.channelSidebar.left - metrics.projectRail.right) > 1) {
failures.push(`expected channel sidebar to sit after project rail, got sidebar.left=${metrics.channelSidebar.left}, rail.right=${metrics.projectRail.right}`)
}
if (Math.abs(metrics.stage.left - metrics.channelSidebar.right) > 1) {
failures.push(`expected stage to sit after channel sidebar, got stage.left=${metrics.stage.left}, sidebar.right=${metrics.channelSidebar.right}`)
}
if (Math.abs(metrics.inspector.left - metrics.stage.right) > 1) {
failures.push(`expected inspector to sit after stage, got inspector.left=${metrics.inspector.left}, stage.right=${metrics.stage.right}`)
}
if (Math.abs(metrics.inspector.right - metrics.viewportWidth) > 1) {
failures.push(`expected inspector to end at viewport right edge, got inspector.right=${metrics.inspector.right}, viewport=${metrics.viewportWidth}`)
}
}
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')
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)}`)
}
for (const [name, overflow] of [
['project rail', metrics.projectRailOverflow],
['project rail children', metrics.projectRailChildrenOverflow],
]) {
if (
!overflow ||
overflow.overflowX !== 'hidden' ||
overflow.overflowY !== 'hidden'
) {
failures.push(`expected ${name} scrollbars to be hidden with no overflow, got ${JSON.stringify(overflow)}`)
}
}
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)}`)
}
if (
!metrics.dashboardButton ||
metrics.dashboardButton.width !== metrics.projectButton?.width ||
metrics.dashboardButton.height !== metrics.projectButton?.height
) {
failures.push(
`dashboard button size ${JSON.stringify(metrics.dashboardButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`,
)
}
if (
!metrics.createProjectButton ||
metrics.createProjectButton.width !== metrics.projectButton?.width ||
metrics.createProjectButton.height !== metrics.projectButton?.height
) {
failures.push(
`create project button size ${JSON.stringify(metrics.createProjectButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`,
)
}
if (failures.length) {
throw new Error(failures.join('\n'))
}